diff --git a/Apps/Frameworks/AGENTS.md b/Apps/Frameworks/AGENTS.md new file mode 100644 index 0000000..6a10cdc --- /dev/null +++ b/Apps/Frameworks/AGENTS.md @@ -0,0 +1,734 @@ +# Dashboard Framework Agent Guide + +This document is for agents building an alternate PowerShell Universal dashboard framework that still speaks the existing PSU app runtime contract. + +This guide documents public behavior and framework mechanics. It intentionally avoids naming PSU server implementation types and focuses on what the framework must send, receive, render, and host. + +## Normative Language + +The keywords `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are normative in this document. + +- `MUST` and `MUST NOT` identify hard compatibility requirements. +- `SHOULD` and `SHOULD NOT` identify strong recommendations that may be violated only with a clear compatibility reason. +- `MAY` identifies optional behavior. + +Target scenario: + +- The PowerShell module emits hashtable descriptors, similar to the current framework. +- The browser UI is implemented with a different React component library such as Ant Design or Semantic UI. +- Static assets are served from a module-provided published folder, not through server-managed framework resolution or the dynamic JavaScript asset route unless lazy plugin loading is explicitly required. + +## Scope + +The reusable contract is not Material UI. The reusable contract is: + +- initial app bootstrap over HTTP +- incremental interaction over SignalR (`/dashboardhub`) +- generic component event execution over HTTP (`/api/internal/component/element/{id}`) +- descriptor materialization via the React registration and render pipeline + +If a replacement framework preserves that contract, it can swap the visual component library without changing the server-side dashboard programming model. + +## Standard Reference Stack + +Future agents `SHOULD` use the same reference stack unless a concrete compatibility problem requires a different choice. + +Required choices: + +- React `MUST` be the component runtime. +- TypeScript `MUST` be used for all framework source code. +- Vite `MUST` be the build tool, dev server, and transpilation pipeline. +- `@microsoft/signalr` `MUST` be the websocket transport client. +- Zustand `MUST` be the client runtime state library. +- `universal-dashboard` `MUST` be referenced when using the stock compatibility wrapper so `withComponentFeatures` comes from the published package instead of a local rewrite. + +Preferred choices: + +- TanStack Query `SHOULD` be used for HTTP request lifecycle management. +- TanStack Router `SHOULD` be used when the framework supports client-side page routing such as `New-UDPage`-style navigation. +- Zod `SHOULD` be used to validate bootstrap payloads, endpoint descriptors, and websocket payloads at the transport boundary. +- React Hook Form `SHOULD` be used for complex framework-owned form components. +- `react-error-boundary` `SHOULD` be used for shell-level and component-level error boundaries. +- Vitest and Testing Library `SHOULD` be the default test stack for unit and component tests. +- Playwright `SHOULD` be used for browser-level integration coverage when validating transport and descriptor behavior end to end. +- MSW `SHOULD` be used for HTTP mocking in unit and component tests. + +Rejected defaults: + +- Redux `SHOULD NOT` be used unless Zustand proves insufficient for a specific requirement. +- Next.js, Remix, or other server-rendering-first frameworks `SHOULD NOT` be used as the shell foundation for this framework. +- React Router or another client-side routing library `SHOULD NOT` be introduced by default when TanStack Router can satisfy the framework's page-routing needs. +- Any client-side routing library `SHOULD NOT` be introduced unless the framework actually owns page navigation beyond a single shell view. +- webpack `SHOULD NOT` be selected for new work when Vite can satisfy the build and dev workflow. + +UI component libraries are intentionally not standardized here. The framework is meant to wrap UI libraries, not prescribe one. Agents `MAY` choose the UI library that best fits the framework being implemented as long as the transport, descriptor, state, and wrapper contracts in this document are preserved. + +## TypeScript Requirements + +TypeScript is mandatory. + +The framework: + +- `MUST` compile from TypeScript source +- `MUST` enable `strict` mode +- `MUST` enable `noUncheckedIndexedAccess` +- `MUST` enable `exactOptionalPropertyTypes` +- `SHOULD` model dashboard descriptors, websocket messages, and endpoint descriptors as explicit types +- `SHOULD` keep transport-layer inputs typed as `unknown` until validated + +The framework `MUST NOT` rely on untyped JavaScript for core transport, state, descriptor, or component-registry logic. + +## Build And Tooling Requirements + +Vite is the standard toolchain. + +The framework: + +- `MUST` use Vite for local development and production builds +- `SHOULD` use the Vite React plugin with SWC for faster iteration +- `SHOULD` produce a static `dist` output suitable for publication through `.universal/publishedFolders.ps1` +- `SHOULD` keep the bundle structure simple enough that future agents can reason about entry points, lazy chunks, and published assets quickly + +Linting and formatting: + +- a single automated linting setup `MUST` be present +- a single automated formatting setup `SHOULD` be present +- agents `SHOULD NOT` introduce multiple competing lint or format pipelines + +## State Ownership Rules + +State ownership must be consistent across all implementations. + +Zustand is the source of truth for runtime client state. + +The Zustand store `MUST` own: + +- shell state such as `dashboardId`, `sessionId`, `pageId`, API base path, and connection status +- descriptor-tree state used to render the page +- component runtime state used by `Set-UDElement` and `Get-UDElement` +- transport status such as reconnecting, connected, disconnected, and last transport error +- transient framework UI state such as snackbars, modals, and progress indicators + +TanStack Query, when used, `MUST NOT` be the source of truth for websocket-driven UI state. + +TanStack Query is not the transport layer. + +TanStack Query `SHOULD` own: + +- bootstrap fetch lifecycle +- idempotent GET requests +- cacheable read operations that are not the live descriptor tree +- retry and stale-time policy for HTTP reads + +Local component state `SHOULD` be limited to purely presentational or ephemeral UI behavior that does not participate in PSU transport semantics. + +Any state needed by `Get-UDElement`, websocket updates, or descriptor-tree mutation `MUST` live in the wrapper or central runtime store, not only in private component state. + +## Transport Architecture Rules + +The framework `MUST` have a dedicated transport layer. + +Components `MUST NOT` call raw `fetch`, raw SignalR connection methods, or download endpoints directly except through the framework transport or wrapper abstractions. + +The transport layer `MUST` expose equivalent capabilities for: + +- bootstrap dashboard data +- connect and reconnect the websocket session +- publish client events +- invoke HTTP endpoints +- persist session-state fallback data +- start file downloads + +The transport layer `SHOULD` validate inputs and outputs at its boundary with Zod or an equivalent schema library. + +The transport layer `MUST NOT` be replaced by TanStack Query alone. Query management and transport are separate concerns. + +The transport layer `SHOULD` be the only layer that knows: + +- raw endpoint URLs +- SignalR connection setup +- retry and reconnect policy +- payload validation and normalization + +If the framework supports page-based client routing, the router layer `MUST` derive its active route state from the dashboard page model and stay consistent with the active `pageId` used for server communication. + +## Recommended Application Architecture + +Future agents `SHOULD` follow this internal module layout even if actual folder names differ: + +- `transport`: HTTP and SignalR client code +- `state`: Zustand stores and store selectors +- `routing`: route definitions and page-to-route resolution when the framework supports multi-page dashboards +- `schema`: Zod schemas and transport payload parsers +- `registry`: component registration and descriptor-to-component resolution +- `components`: framework UI components +- `features`: optional higher-level areas such as forms, tables, modals, and notifications +- `testing`: test utilities, mocks, and integration harnesses + +The framework `SHOULD` preserve clear boundaries: + +- components render UI and raise events +- wrapper logic adapts descriptors into component props and callbacks +- state stores own runtime state +- routing resolves page navigation and URL state when multi-page dashboards are enabled +- transport owns server communication +- schemas own runtime validation + +## Modern Conventions For All Agents + +Future agents `SHOULD` apply these conventions consistently. + +- Prefer schema validation at network boundaries over trusting server payload shape implicitly. +- Prefer small composable hooks over monolithic controller components. +- Prefer store selectors over broad whole-store subscriptions. +- Prefer explicit typed descriptor models over anonymous `Record` structures. +- Prefer deterministic fallback rendering over silent failure for unknown component types. +- Prefer command-query separation: websocket and POST endpoint operations mutate state, while GET operations read state. +- Prefer central reconnection handling instead of per-component websocket recovery logic. +- Prefer lazy loading only for optional plugin components, not for the primary shell. +- Prefer one obvious state path for runtime behavior; avoid duplicating the same state across query cache, local state, and a global store. + +## Testing Expectations + +The framework `SHOULD` include three levels of testing. + +- unit tests for descriptor parsing, schemas, selectors, and transport helpers +- component tests for wrapped components, state updates, and endpoint callback behavior +- browser or integration tests for bootstrap, websocket flows, downloads, and tree mutation behaviors + +The minimum useful automated coverage `SHOULD` prove: + +- bootstrap from `/api/internal/dashboard` +- websocket connection and required message handling +- `Set-UDElement` and `Get-UDElement` behavior +- descriptor-tree mutation behavior for `addElement`, `clearElement`, `removeElement`, and `syncElement` +- download handling from `/api/internal/dashboard/download/{dashboardId}/{id}` + +## Development Harness + +Framework development and browser-level testing `SHOULD` use the local harness in `Apps/Frameworks/Harness` instead of requiring a full PSU runtime. + +The harness exists to host framework bundles against the documented dashboard contract with only the minimum server behavior a framework needs: + +- static asset hosting through published-folder style request paths +- bootstrap over `/api/internal/dashboard` +- HTTP component execution over `/api/internal/component/element/{id}` +- session-state fallback over `/api/internal/component/element/sessionState/{requestId}` +- downloads over `/api/internal/dashboard/download/{dashboardId}/{id}` +- SignalR traffic over `/dashboardhub` +- PowerShell-backed endpoint and event execution for local iteration + +Agent guidance: + +- framework authors `SHOULD` point their local shell, bundle, and static assets at the harness during development +- Playwright and other browser-level tests `SHOULD` run against the harness by default +- agents `SHOULD NOT` require a full PSU instance just to validate framework transport behavior, endpoint execution, or websocket message handling unless a test explicitly depends on PSU-only features outside this contract +- if harness behavior is insufficient for a new framework need, extend the harness before introducing PSU runtime setup as the default workflow + +## Asset Hosting Rules + +Baseline context: + +- the stock framework ships a built-in client bundle and supports plugin-style lazy component loading +- plugin-style components can mark descriptors with `isPlugin = $true` and `assetId`, which causes the browser to fetch `/api/internal/javascript/{assetId}?dashboardId={id}`. +- the fetched script is expected to register components through `window.UniversalDashboard.register(type, component)`. + +Conformance rules: + +- A new framework `MUST NOT` depend on server-side framework resolution. +- A new framework `MUST NOT` depend on `/api/internal/javascript/{asset}` for its normal bundle. +- A new framework `MUST` ship its compiled JS and CSS in the framework module. +- A new framework `MUST` expose those files through a module-provided `.universal/publishedFolders.ps1`. +- A new framework `MUST` load its primary bundle from the published folder like any other static asset. + +The dynamic JavaScript asset endpoint `MAY` be used only for intentional plugin-style lazy loading by `assetId`. + +## Static Asset Model + +Published folders are the required hosting model for framework-owned static assets. + +Framework assumptions: + +- each published folder exposes a catch-all route from its configured `RequestPath` +- the route shape is `/{requestPath}/{**subPath}` +- the server serves the underlying files from the configured folder path +- path traversal is blocked before dispatch + +Example declaration from `.universal/publishedFolders.ps1`: + +```powershell +New-PSUPublishedFolder -Name 'PublishedFolder' -RequestPath '/images' -Path 'C:\images' -DefaultDocument @('') -Authentication -Role @('Administrator') +``` + +For a framework module, the same pattern can expose a built UI bundle: + +```powershell +New-PSUPublishedFolder -Name 'MyFrameworkAssets' ` + -RequestPath '/frameworks/my-framework' ` + -Path "$PSScriptRoot/../dist" +``` + +Then the app can reference assets such as: + +- `/frameworks/my-framework/app.js` +- `/frameworks/my-framework/app.css` + +Conformance rules: + +- Framework-owned JS, CSS, images, fonts, and similar static assets `MUST` be delivered from a published folder. +- A framework `SHOULD` use stable, framework-specific request paths such as `/frameworks/my-framework`. +- A framework `SHOULD NOT` route ordinary static assets through dynamic component-loading endpoints. +- A framework `MAY` use authenticated or role-protected published folders when the framework requires restricted access. + +## Browser Bootstrap Contract + +The browser shell requires two pieces of routing state from meta tags: + +- `baseurl` controls the API prefix +- `ud-dashboard` provides the dashboard id when local storage does not override it + +If a replacement frontend hosts its own shell page, it still needs equivalents for: + +- API base path +- dashboard id +- current location and base URL handling + +The first required HTTP call is: + +```text +GET /api/internal/dashboard +``` + +The response includes: + +- `dashboard`: the root descriptor tree for the page +- `sessionId`: dashboard session identifier +- `pageId`: current page identifier +- `authType` +- `roles` +- `user` +- `idleTimeout` +- `dashboardName` +- `developerLicense` + +Conformance rules: + +- The browser shell `MUST` perform `GET /api/internal/dashboard` before initial render. +- The browser shell `MUST` treat `dashboard` as the root descriptor tree. +- The browser shell `MUST` preserve `sessionId` and `pageId` for subsequent websocket communication. +- The browser shell `SHOULD` preserve `authType`, `roles`, `user`, and timeout-related values for any UI or behavior that depends on them. +- The browser shell `MUST` honor the effective API base path when constructing all API requests. + +## Websocket Contract + +The browser connects to SignalR at: + +```text +/dashboardhub?dashboardid={dashboardId}&pageid={pageId}&sessionid={sessionId}&timezone={timezone} +``` + +The server sends websocket messages identified by a `messageType` and payload. Some interactions are fire-and-forget, and some expect a direct result, such as state requests. + +Extended message set: + +- `setState` +- `requestState` +- `addElement` +- `clearElement` +- `removeElement` +- `syncElement` +- `download` +- `redirect` +- `showToast` +- `showSnackbar` +- `hideSnackbar` +- `showModal` +- `closeModal` +- `invoke` +- `invokeMethod` +- `invokejavascript` +- `invokejavascriptreturn` +- `progress` +- `clipboard` +- `select` +- `refresh` +- `write` +- `log` + +Required baseline message set: + +- `setState` +- `requestState` +- `addElement` +- `clearElement` +- `removeElement` +- `syncElement` +- `download` +- `redirect` + +Conformance rules: + +- The browser shell `MUST` connect to `/dashboardhub` using `dashboardId`, `pageId`, `sessionId`, and `timezone`. +- The browser shell `MUST` implement the required baseline message set. +- The browser shell `SHOULD` implement the extended message set when parity with the stock experience is required. +- The browser shell `MUST` return state for request-response websocket flows that expect a direct result. +- The browser shell `MUST` preserve the message type names exactly as documented here. + +## Descriptor Materialization + +The browser-side rendering contract is based on a simple registry-and-render pattern. + +Render pipeline: + +1. The root descriptor returned by `/api/internal/dashboard` is passed into the render service. +2. The render service locates a registered React component by `component.type`. +3. If found, it renders that component with the descriptor spread as props. +4. If the descriptor is marked `isPlugin` and the component is not already loaded, the browser MAY lazily load a script from `/api/internal/javascript/{assetId}` and retry rendering. +5. If no matching component exists, the framework MUST render a deterministic fallback or error component. + +Conformance rules: + +- PowerShell descriptor output `MUST` remain plain serializable objects. +- The browser `MUST` map `type` to a registered React component or equivalent render target. +- Child content `MUST` remain recursively renderable descriptors or primitive values. +- A framework `MUST NOT` change descriptor semantics merely to match a UI library preference. + +The replacement UI library changes how the React component renders, not how the descriptor is shaped. + +## `withComponentFeatures` + +A compatibility helper named `withComponentFeatures` is the main wrapper used to make framework components behave like PSU dashboard components. Its behavior is normative even if the implementation is rewritten. + +Preferred source: + +- agents `SHOULD` import `withComponentFeatures` from the `universal-dashboard` npm package when the package satisfies the framework's compatibility needs +- agents `SHOULD NOT` reimplement `withComponentFeatures` unless a concrete compatibility gap in `universal-dashboard` forces that divergence + +Required wrapper capabilities: + +- `render(component)` +- `setState(state)` +- `publish(topic, payload)` +- `notifyOfEvent(eventName, eventData)` +- `post(path, body)` +- `get(path)` +- `subscribeToIncomingEvents(callback)` +- `unsubscribeFromIncomingEvents(token)` +- `newEndpoint(endpoint)` + +Required incoming event handling: + +- `setState` +- `getState` +- `requestState` +- `addElement` +- `clearElement` +- `removeElement` +- `syncElement` + +Conformance rules: + +- A conforming framework `MUST` provide behavior equivalent to `withComponentFeatures`. +- A conforming framework `SHOULD` use `import { withComponentFeatures } from 'universal-dashboard'` rather than maintaining a forked local wrapper. +- Interactive components `MUST` receive wrapped endpoint callbacks instead of raw endpoint descriptor objects. +- Interactive components `MUST` be able to receive incoming events addressed to their component id. +- State that must participate in request-state flows `MUST` be tracked through the wrapper state mechanism, not only local UI state. + +## Endpoint Handling + +PowerShell event handlers arrive in descriptors as endpoint-shaped objects. The wrapper converts those descriptors into callable JavaScript functions. + +The client-side wrapper distinguishes three cases. + +### 1. Inline JavaScript endpoint + +If the endpoint contains `javaScript`, the wrapper builds: + +```javascript +new Function('data', endpoint.javaScript) +``` + +This path `SHOULD` be used only when the server intentionally emitted client-side JavaScript. + +### 2. Websocket endpoint + +If the endpoint contains `websocket`, the wrapper publishes an `element-event` message with: + +- `type: 'clientEvent'` +- `eventId: endpoint.name` +- `eventName: endpoint.name` +- `eventData: data` + +The browser shell `MUST` bridge that PubSub event to SignalR by invoking the hub `event` method and adding the current browser location. + +This is the low-latency path used by `Set-UDElement`, `Get-UDElement`, downloads, and other server-driven interactions. + +### 3. HTTP endpoint + +Otherwise the wrapper issues: + +```text +POST /api/internal/component/element/{endpoint.name} +``` + +Optional endpoint metadata influences the request: + +- `accept` sets the `Accept` header +- `contentType` sets the `Content-Type` header + +The HTTP path SHOULD append a query string only when the caller passes `options.query`, while still sending the main payload as the request body. + +The framework must treat these as the generic component execution endpoints: + +- `GET /api/internal/component/element/{id}` +- `POST /api/internal/component/element/{id}` + +Conformance rules: + +- An endpoint descriptor with `javaScript` `MUST` execute in the browser. +- An endpoint descriptor with `websocket` `MUST` publish a client event over the websocket path. +- Any other endpoint descriptor `MUST` issue an HTTP request to `/api/internal/component/element/{endpoint.name}`. +- The HTTP endpoint path `MUST` preserve `Accept` and `Content-Type` metadata when provided. +- The HTTP endpoint path `SHOULD` support optional query-string augmentation through `options.query`. +- Component execution endpoints `MUST` support JSON, plain text, and multipart form data. + +## Generic Interactive Cmdlet Semantics + +These cmdlets are not framework-specific. A conforming framework `MUST` preserve the browser behavior they rely on. + +### `Set-UDElement` + +Behavior: + +- sends websocket message type `setState` +- payload includes `componentId` and `state` +- if `-Content` is provided, child content is materialized and assigned into `state.content` + +Framework requirement: + +- when the component receives `setState` for its id, it `MUST` merge or replace local state and rerender + +### `Get-UDElement` + +Behavior: + +- creates a request id +- sends websocket message type `requestState` +- waits for direct websocket result +- if the direct result is `null`, falls back to persisted session state lookup + +Framework requirement: + +- on `requestState`, the component `MUST` read its current component state and return it +- if direct return is not possible, the framework `MUST` support the session state fallback route below + +Session-state fallback endpoint: + +```text +POST /api/internal/component/element/sessionState/{requestId} +``` + +### `Invoke-UDDownload` / `Start-UDDownload` + +Behavior: + +- server stores a temporary download in the dashboard session +- server sends websocket message type `download` with `id` and `fileName` +- browser downloads from: + +```text +GET /api/internal/dashboard/download/{dashboardId}/{id} +``` + +Framework requirement: + +- create an anchor or equivalent browser download action when `download` arrives + +### `Remove-UDElement`, `Clear-UDElement`, `Add-UDElement`, `Sync-UDElement` + +These are all transport-level operations on the descriptor tree. + +Browser requirements: + +- `removeElement`: `MUST` delete one child or target element +- `clearElement`: `MUST` remove children or content from a target +- `addElement`: `MUST` append or insert new descriptor content +- `syncElement`: `MUST` replace or refresh a target subtree with new descriptor content + +If these are implemented correctly, many higher-level PowerShell cmdlets work automatically. + +## Component Authoring Rules + +For a replacement React framework: + +1. Each component type `MUST` be registered with a framework registry such as `registerComponent(type, component)` or an equivalent API. +2. Interactive components `MUST` be wrapped with `withComponentFeatures` or an equivalent compatibility wrapper. +3. Descriptor props `MUST` be treated as the source of truth for: + - `id` + - child `content` + - event endpoints such as `onClick`, `onChange`, or custom handlers +4. PSU endpoint descriptors `MUST` be converted into real JS callbacks through `newEndpoint(...)` or equivalent behavior. +5. Child rendering `MUST` remain recursive through the provided `render(...)` helper or equivalent behavior. + +Normative example shape: + +```javascript +import React from 'react'; +import { Button } from 'antd'; +import { withComponentFeatures } from 'universal-dashboard'; +import { registerComponent } from '../registry'; + +const UDButton = props => { + const onClick = props.onClick; + + return ( + + ); +}; + +registerComponent('my-button', withComponentFeatures(UDButton)); +``` + +The UI library choice is not the compatibility boundary. The compatibility boundary is that the wrapper turns a PowerShell endpoint descriptor into an executable callback while preserving recursive rendering and incoming state updates. + +## Extracted Example Patterns + +The examples below are intentionally self-contained so future agents can reason about the framework contract without opening the stock implementation. + +### Example PowerShell descriptor + +This is the general shape of a PowerShell component descriptor: + +```powershell +function New-UDExampleButton { + param( + [string]$Id = [Guid]::NewGuid(), + [string]$Text, + [Endpoint]$OnClick + ) + + $OnClick.Register($Id, $PSCmdlet) + + @{ + type = 'example-button' + id = $Id + text = $Text + onClick = $OnClick + } +} +``` + +Normative interpretation: + +- `type` `MUST` select the browser component +- `id` `MUST` be the address used for incoming state and tree mutation messages +- endpoint-valued properties such as `onClick` `MUST` be converted into callable JavaScript callbacks by the framework wrapper +- additional scalar properties `MUST` become normal component props unless the component contract says otherwise + +### Example endpoint descriptor behavior + +An endpoint property should be treated as an object descriptor, not as an already-executable function. The wrapper is responsible for turning it into a callable callback. + +Conformance rules: + +- if the endpoint declares `javaScript`, execute that JavaScript in the browser +- if the endpoint declares `websocket`, publish a client event over the websocket path +- otherwise POST to `/api/internal/component/element/{endpoint.name}` + +### Example wrapped React component + +```javascript +const ExampleButton = props => { + const onClick = props.onClick; + + return ( + + ); +}; + +registerComponent('example-button', withComponentFeatures(ExampleButton)); +``` + +Normative interpretation: + +- the component `MUST` read ordinary descriptor props like `id` and `text` +- event props like `onClick` `MUST` already be wrapped into callable functions before component use +- child descriptors `MUST` be rendered recursively through the provided `render(...)` helper rather than handled as raw objects + +### Example stateful component pattern + +If a component exposes state that should be visible to `Get-UDElement`, that state `MUST` be updated through the wrapper's `setState(...)` helper instead of being managed only in local React state. + +```javascript +const ExampleInput = props => { + return ( + props.setState({ value: event.target.value })} + /> + ); +}; +``` + +This ensures the browser-side component state remains available to request-state flows. + +## When To Use `assetId` And `/api/internal/javascript` + +This path `MAY` be used only when all of these are true: + +- a component is loaded lazily after initial bundle execution +- the descriptor includes `isPlugin = $true` +- the descriptor includes `assetId` +- the script returned by `/api/internal/javascript/{assetId}` will call `UniversalDashboard.register(...)` + +This path `MUST NOT` be used for: + +- the main framework bundle +- CSS delivery +- normal static images, fonts, and icons +- deterministic framework scripts that can be shipped through a published folder + +For a full alternate framework, one static published-folder bundle that registers the complete component set up front `SHOULD` be the default design. + +## Conformance Checklist + +A framework is conforming only if it provides all of the following: + +- a way to obtain `dashboardId`, `sessionId`, `pageId`, and API base path +- an HTTP bootstrap call to `/api/internal/dashboard` +- a SignalR connection to `/dashboardhub` +- a `type` to React component registry +- recursive rendering of child descriptors +- `withComponentFeatures` behavior, either by reuse or equivalent reimplementation +- support for websocket messages `setState`, `requestState`, `addElement`, `clearElement`, `removeElement`, `syncElement`, `download`, and `redirect` +- support for HTTP endpoint execution through `/api/internal/component/element/{id}` +- static asset delivery through `.universal/publishedFolders.ps1` + +## Suggested Delivery Sequence + +1. Start the harness in `Apps/Frameworks/Harness` and host the framework bundle there. +2. Serve a minimal bundle from a published folder or harness-mounted static path. +3. Fetch `/api/internal/dashboard` and render static descriptors. +4. Reuse or reimplement `withComponentFeatures`. +5. Support HTTP endpoint invocation. +6. Support websocket connection and `setState`. +7. Add `requestState`, `download`, and descriptor tree mutation messages. +8. Add optional UX messages such as toast, modal, clipboard, and custom JS invocation. + +## Agent Guidance + +- Start from the transport contract, not from the current MUI components. +- Treat `withComponentFeatures`, recursive descriptor rendering, and wrapped endpoint callbacks as the compatibility surface for component behavior. +- Treat the websocket message names and payload shapes documented here as the source of truth for browser-side reactions. +- Treat the documented HTTP endpoints and payloads as the source of truth for server communication. +- Use `Apps/Frameworks/Harness` as the default development and Playwright host for framework work. +- Use published folders for the framework's compiled assets unless lazy plugin loading is explicitly needed. +- Do not assume the server will resolve and host your framework bundle automatically; ship the bundle with the module and expose it explicitly through a published folder. +- Assume future agents may only have this document. Keep framework work anchored in the contracts and examples captured here rather than in private implementation details. + +If a future implementation needs to diverge from the built-in dashboard shell completely, it `MUST` preserve the HTTP and websocket contracts first. That is the compatibility boundary that keeps PowerShell cmdlets and descriptor generation useful. \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/.gitignore b/Apps/Frameworks/AntDesign/.gitignore new file mode 100644 index 0000000..c2ec0fc --- /dev/null +++ b/Apps/Frameworks/AntDesign/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.tsbuildinfo +playwright-report/ +test-results/ diff --git a/Apps/Frameworks/AntDesign/.universal/publishedFolders.ps1 b/Apps/Frameworks/AntDesign/.universal/publishedFolders.ps1 new file mode 100644 index 0000000..3ba1531 --- /dev/null +++ b/Apps/Frameworks/AntDesign/.universal/publishedFolders.ps1 @@ -0,0 +1,5 @@ +$assetPath = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\dist')) + +if (Test-Path -LiteralPath $assetPath) { + New-PSUPublishedFolder -Name 'AntDesign Dashboard Framework' -RequestPath '/frameworks/ant-design' -Path $assetPath -DefaultDocument @('index.html') +} diff --git a/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psd1 b/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psd1 new file mode 100644 index 0000000..f62b901 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psd1 @@ -0,0 +1,38 @@ +@{ + RootModule = 'Devolutions.PowerShellUniversal.Frameworks.AntDesign.psm1' + ModuleVersion = '0.1.0' + GUID = '458668d8-d3f4-4f68-bdc5-4f24836b4bd5' + Author = 'Devolutions, Inc.' + CompanyName = 'Devolutions, Inc.' + Copyright = '(c) Devolutions, Inc. All rights reserved.' + Description = 'Ant Design dashboard framework scaffold for PowerShell Universal.' + FunctionsToExport = @( + 'Get-PSUAntDesignFrameworkAssetBasePath', + 'Get-PSUAntDesignFrameworkEntryPoint', + 'New-UDAntDesignText', + 'New-UDAntDesignButton', + 'New-UDAntDesignCheckbox', + 'New-UDAntDesignCol', + 'New-UDAntDesignInput', + 'New-UDAntDesignLayout', + 'New-UDAntDesignLayoutContent', + 'New-UDAntDesignLayoutFooter', + 'New-UDAntDesignLayoutHeader', + 'New-UDAntDesignLayoutSider', + 'New-UDAntDesignRate', + 'New-UDAntDesignRow', + 'New-UDAntDesignSwitch', + 'New-UDAntDesignTypography', + 'Show-AntDesignMessage', + 'New-AntDesignDemo', + 'New-AntDesignDemoApp' + ) + PrivateData = @{ + PSData = @{ + Tags = @('PowerShellUniversal', 'framework', 'app', 'dashboard', 'antd') + LicenseUri = 'https://github.com/devolutions/powershell-universal-gallery/blob/main/LICENSE' + ProjectUri = 'https://github.com/devolutions/powershell-universal-gallery/tree/main/Apps/Frameworks/AntDesign' + IconUri = 'https://raw.githubusercontent.com/devolutions/powershell-universal-gallery/main/images/app.png' + } + } +} diff --git a/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psm1 b/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psm1 new file mode 100644 index 0000000..4def153 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Devolutions.PowerShellUniversal.Frameworks.AntDesign.psm1 @@ -0,0 +1,40 @@ +$privatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' +$publicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' + +if (Test-Path -Path $privatePath) { + Get-ChildItem -Path $privatePath -Filter '*.ps1' -File | + Sort-Object -Property Name | + ForEach-Object { + . $_.FullName + } +} + +if (Test-Path -Path $publicPath) { + Get-ChildItem -Path $publicPath -Filter '*.ps1' -File | + Sort-Object -Property Name | + ForEach-Object { + . $_.FullName + } +} + +Export-ModuleMember -Function @( + 'Get-PSUAntDesignFrameworkAssetBasePath', + 'Get-PSUAntDesignFrameworkEntryPoint', + 'New-UDAntDesignText', + 'New-UDAntDesignButton', + 'New-UDAntDesignCheckbox', + 'New-UDAntDesignCol', + 'New-UDAntDesignInput', + 'New-UDAntDesignLayout', + 'New-UDAntDesignLayoutContent', + 'New-UDAntDesignLayoutFooter', + 'New-UDAntDesignLayoutHeader', + 'New-UDAntDesignLayoutSider', + 'New-UDAntDesignRate', + 'New-UDAntDesignRow', + 'New-UDAntDesignSwitch', + 'New-UDAntDesignTypography', + 'Show-AntDesignMessage', + 'New-AntDesignDemo', + 'New-AntDesignDemoApp' +) diff --git a/Apps/Frameworks/AntDesign/Private/Documentation.ps1 b/Apps/Frameworks/AntDesign/Private/Documentation.ps1 new file mode 100644 index 0000000..e0047bb --- /dev/null +++ b/Apps/Frameworks/AntDesign/Private/Documentation.ps1 @@ -0,0 +1,302 @@ +function Get-AntDesignHelpBlock { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$CommandName + ) + + $command = Get-Command -Name $CommandName -ErrorAction Stop + $commandSourcePath = $command.ScriptBlock.Ast.Extent.File + + if ([string]::IsNullOrWhiteSpace($commandSourcePath)) { + throw "Unable to locate source file for $CommandName." + } + + if (-not $script:AntDesignModuleSourceByPath) { + $script:AntDesignModuleSourceByPath = @{} + } + + if (-not $script:AntDesignModuleSourceByPath.ContainsKey($commandSourcePath)) { + $script:AntDesignModuleSourceByPath[$commandSourcePath] = Get-Content -Path $commandSourcePath -Raw + } + + $pattern = "(?ms)function\s+$([regex]::Escape($CommandName))\s*\{\s*<#(?.*?)#>" + $match = [regex]::Match($script:AntDesignModuleSourceByPath[$commandSourcePath], $pattern) + + if (-not $match.Success) { + throw "Unable to locate comment-based help for $CommandName." + } + + $match.Groups['help'].Value +} + +function ConvertFrom-AntDesignExampleBlock { + [CmdletBinding()] + param( + [string[]]$Lines, + [int]$Index + ) + + $content = [System.Collections.Generic.List[string]]::new() + + foreach ($line in $Lines) { + $content.Add($line.TrimEnd()) + } + + while ($content.Count -gt 0 -and [string]::IsNullOrWhiteSpace($content[0])) { + $content.RemoveAt(0) + } + + while ($content.Count -gt 0 -and [string]::IsNullOrWhiteSpace($content[$content.Count - 1])) { + $content.RemoveAt($content.Count - 1) + } + + $title = "Example $Index" + + if ($content.Count -gt 0 -and $content[0] -match '^#\s*(.+)$') { + $title = $Matches[1].Trim() + $content.RemoveAt(0) + } + + $splitIndex = -1 + + for ($lineIndex = 0; $lineIndex -lt $content.Count; $lineIndex++) { + if ([string]::IsNullOrWhiteSpace($content[$lineIndex])) { + $splitIndex = $lineIndex + break + } + } + + if ($splitIndex -ge 0) { + $codeLines = @($content.GetRange(0, $splitIndex)) + $descriptionLines = @($content.GetRange($splitIndex + 1, $content.Count - $splitIndex - 1)) + } + else { + $codeLines = @($content) + $descriptionLines = @() + } + + [ordered]@{ + title = $title + code = ($codeLines -join [Environment]::NewLine).Trim() + description = ($descriptionLines -join ' ').Trim() + } +} + +function ConvertFrom-AntDesignHelpList { + [CmdletBinding()] + param( + [string]$Text + ) + + if ([string]::IsNullOrWhiteSpace($Text)) { + return @() + } + + @( + $Text -split "`r?`n" | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { $_ -replace '^[\-\*\u2022]\s*', '' } + ) +} + +function ConvertFrom-AntDesignHelpBlock { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$CommandName + ) + + $helpBlock = Get-AntDesignHelpBlock -CommandName $CommandName + $lines = @($helpBlock -split "`r?`n" | ForEach-Object { $_.TrimEnd() }) + + $result = [ordered]@{ + Synopsis = '' + Description = '' + Notes = '' + Parameters = @{} + Examples = @() + } + + $currentSection = $null + $currentName = $null + $buffer = [System.Collections.Generic.List[string]]::new() + + function Save-AntDesignHelpSection { + param( + [string]$Section, + [string]$Name, + [System.Collections.Generic.List[string]]$SectionBuffer + ) + + if ([string]::IsNullOrWhiteSpace($Section)) { + return + } + + $value = ($SectionBuffer.ToArray() -join [Environment]::NewLine).Trim() + + if ([string]::IsNullOrWhiteSpace($value)) { + return + } + + switch ($Section) { + 'Synopsis' { + $result['Synopsis'] = $value + } + 'Description' { + $result['Description'] = $value + } + 'Notes' { + $result['Notes'] = $value + } + 'Parameter' { + $result.Parameters[$Name] = ($SectionBuffer.ToArray() -join ' ').Trim() + } + 'Example' { + $result['Examples'] += ,(ConvertFrom-AntDesignExampleBlock -Lines $SectionBuffer.ToArray() -Index ($result['Examples'].Count + 1)) + } + } + } + + foreach ($line in ($lines + '.END')) { + $trimmedLine = $line.TrimStart() + + if ($trimmedLine -match '^\.(\w+)(?:\s+(.+))?$') { + Save-AntDesignHelpSection -Section $currentSection -Name $currentName -SectionBuffer $buffer + $buffer.Clear() + + switch ($Matches[1].ToUpperInvariant()) { + 'SYNOPSIS' { + $currentSection = 'Synopsis' + $currentName = $null + } + 'DESCRIPTION' { + $currentSection = 'Description' + $currentName = $null + } + 'NOTES' { + $currentSection = 'Notes' + $currentName = $null + } + 'PARAMETER' { + $currentSection = 'Parameter' + $currentName = $Matches[2] + } + 'EXAMPLE' { + $currentSection = 'Example' + $currentName = $null + } + Default { + $currentSection = $null + $currentName = $null + } + } + + continue + } + + $buffer.Add($trimmedLine) + } + + $result +} + +function Invoke-AntDesignDocumentationExample { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Code + ) + + if ([string]::IsNullOrWhiteSpace($Code)) { + return $null + } + + try { + & ([scriptblock]::Create($Code)) + } + catch { + New-UDAntDesignText -Text "Example preview failed: $($_.Exception.Message)" + } +} + +function Get-AntDesignCommandParameters { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$CommandName, + [hashtable]$HelpParameters = @{} + ) + + $commonParameters = @( + 'Verbose', + 'Debug', + 'ErrorAction', + 'WarningAction', + 'InformationAction', + 'ProgressAction', + 'ErrorVariable', + 'WarningVariable', + 'InformationVariable', + 'OutVariable', + 'OutBuffer', + 'PipelineVariable' + ) + + $command = Get-Command -Name $CommandName -ErrorAction Stop + + foreach ($parameter in $command.Parameters.Values) { + if ($parameter.Name -in $commonParameters) { + continue + } + + $parameterAttribute = $parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | Select-Object -First 1 + $validateSetAttribute = $parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } | Select-Object -First 1 + + [ordered]@{ + name = $parameter.Name + type = $parameter.ParameterType.Name + required = [bool]($null -ne $parameterAttribute -and $parameterAttribute.Mandatory) + description = $HelpParameters[$parameter.Name] + validValues = if ($null -ne $validateSetAttribute) { @($validateSetAttribute.ValidValues) } else { @() } + } + } +} + +function Get-AntDesignComponentDocumentation { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Key, + [Parameter(Mandatory)] + [string]$Title, + [Parameter(Mandatory)] + [string]$CommandName, + [string]$Category = 'General', + [string]$SourceUrl + ) + + $help = ConvertFrom-AntDesignHelpBlock -CommandName $CommandName + $examples = foreach ($example in $help.Examples) { + [ordered]@{ + title = $example.title + description = $example.description + code = $example.code + preview = Invoke-AntDesignDocumentationExample -Code $example.code + } + } + + [ordered]@{ + key = $Key + title = $Title + category = $Category + commandName = $CommandName + summary = $help.Synopsis + description = $help.Description + whenToUse = @(ConvertFrom-AntDesignHelpList -Text $help.Notes) + sourceUrl = $SourceUrl + parameters = @(Get-AntDesignCommandParameters -CommandName $CommandName -HelpParameters $help.Parameters) + examples = @($examples) + } +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Button.ps1 b/Apps/Frameworks/AntDesign/Public/Button.ps1 new file mode 100644 index 0000000..769e980 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Button.ps1 @@ -0,0 +1,322 @@ +function New-UDAntDesignButton { + <# + .SYNOPSIS + Creates an Ant Design button descriptor. + + .DESCRIPTION + Creates an antd-button descriptor that maps the PowerShell command surface to the core Ant Design Button TypeScript definition used by the client runtime. The command mirrors the Ant Design Button API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs. + + .NOTES + A button represents an operation or a short sequence of operations. + Use a primary button for the main action in a section, and keep it to one primary action when possible. + Use default buttons for secondary actions, dashed buttons for add-more style actions, text buttons for the least prominent actions, and link buttons for navigation. + Use danger for destructive actions, ghost when the button sits on a strong background, disabled when the action is unavailable, and loading to prevent repeated submissions. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Text + Specifies the plain text content rendered inside the button. + + .PARAMETER Content + Specifies descriptor content rendered inside the button when you need more than plain text. + + .PARAMETER Icon + Specifies descriptor content rendered in the Ant Design icon slot. + + .PARAMETER Type + Specifies the legacy Ant Design button type shortcut. Valid values are default, primary, dashed, link, and text. + + .PARAMETER Color + Specifies the Ant Design button color token. + + .PARAMETER Variant + Specifies the Ant Design button variant. + + .PARAMETER Shape + Specifies the button shape. + + .PARAMETER Size + Specifies the button size. + + .PARAMETER Disabled + Disables the button. + + .PARAMETER Loading + Shows the Ant Design loading state. + + .PARAMETER LoadingDelay + Delays the loading spinner by the specified number of milliseconds. + + .PARAMETER LoadingIcon + Specifies descriptor content or an Ant Design icon name rendered inside the loading indicator. + + .PARAMETER Ghost + Renders the button with Ant Design ghost styling. + + .PARAMETER Danger + Applies the Ant Design danger treatment. + + .PARAMETER Block + Expands the button to the full available width. + + .PARAMETER Href + Renders the button as a link when specified. + + .PARAMETER HtmlType + Specifies the underlying HTML button type. + + .PARAMETER AutoInsertSpace + Controls Ant Design automatic spacing for two Chinese characters. + + .PARAMETER IconPosition + Specifies whether the icon renders before or after the button content. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design button element. + + .PARAMETER RootClassName + Specifies a root class name applied by Ant Design. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered button. Keys are emitted as data-* attributes. + + .PARAMETER OnClick + Specifies the endpoint invoked when the button is clicked. + + .PARAMETER Value + Specifies the value sent back through the click event payload. + + .EXAMPLE + # Syntactic sugar + @( + New-UDAntDesignButton -Text 'Primary Button' -Type primary + New-UDAntDesignButton -Text 'Default Button' -Type default + New-UDAntDesignButton -Text 'Dashed Button' -Type dashed + New-UDAntDesignButton -Text 'Text Button' -Type text + New-UDAntDesignButton -Text 'Link Button' -Type link -Href 'https://ant.design/components/button/' + ) + + Uses the Ant Design type shortcuts to mirror the core button styles from the upstream docs. + + .EXAMPLE + # Color and variant + @( + New-UDAntDesignButton -Text 'Primary Solid' -Color primary -Variant solid + New-UDAntDesignButton -Text 'Default Outlined' -Color default -Variant outlined + New-UDAntDesignButton -Text 'Success Filled' -Color green -Variant filled + New-UDAntDesignButton -Text 'Danger Text' -Color danger -Variant text -Danger + New-UDAntDesignButton -Text 'Geekblue Link' -Color geekblue -Variant link -Href 'https://ant.design/components/button/' + ) + + Combines color and variant to show the newer styling model behind the Ant Design type aliases. + + .EXAMPLE + # Icon + @( + New-UDAntDesignButton -Text 'Search Primary' -Type primary -Icon 'SearchOutlined' + New-UDAntDesignButton -Text 'Download File' -Icon 'DownloadOutlined' + New-UDAntDesignButton -Text 'External Link' -Type link -Icon 'LinkOutlined' -Href 'https://ant.design/components/button/' + ) + + Adds Ant Design icons through the PowerShell command surface while keeping the same button API. + + .EXAMPLE + # Icon placement + @( + New-UDAntDesignButton -Text 'Search Start' -Type primary -Icon 'SearchOutlined' -IconPosition start + New-UDAntDesignButton -Text 'Search End' -Type primary -Icon 'SearchOutlined' -IconPosition end + ) + + Moves the icon before or after the label to match the icon placement examples from the docs. + + .EXAMPLE + # Size + @( + New-UDAntDesignButton -Text 'Large Action' -Type primary -Size large + New-UDAntDesignButton -Text 'Medium Action' -Type default -Size middle + New-UDAntDesignButton -Text 'Small Action' -Type dashed -Size small + ) + + Shows the three Ant Design button sizes exposed by the PowerShell wrapper. + + .EXAMPLE + # Disabled + @( + New-UDAntDesignButton -Text 'Primary Disabled' -Type primary -Disabled + New-UDAntDesignButton -Text 'Default Disabled' -Type default -Disabled + New-UDAntDesignButton -Text 'Dashed Disabled' -Type dashed -Disabled + New-UDAntDesignButton -Text 'Text Disabled' -Type text -Disabled + New-UDAntDesignButton -Text 'Link Disabled' -Type link -Disabled -Href 'https://ant.design/components/button/' + ) + + Disables each style to document the unavailable state consistently. + + .EXAMPLE + # Loading + @( + New-UDAntDesignButton -Text 'Saving Changes' -Type primary -Loading + New-UDAntDesignButton -Text 'Queued Request' -Loading -LoadingDelay 400 + New-UDAntDesignButton -Text 'Syncing Data' -Type default -Loading -LoadingIcon 'LoadingOutlined' + ) + + Uses loading states to communicate in-progress work and to discourage repeated clicks. + + .EXAMPLE + # Multiple buttons + @( + New-UDAntDesignButton -Text 'Primary Action' -Type primary + New-UDAntDesignButton -Text 'Secondary Action' -Type default + New-UDAntDesignButton -Text 'More Actions' -Type dashed -Icon 'EllipsisOutlined' + ) + + Follows the Ant Design guidance of one primary action plus secondary actions in the same group. + + .EXAMPLE + # Ghost button + @( + New-UDAntDesignButton -Text 'Ghost Primary' -Type primary -Ghost + New-UDAntDesignButton -Text 'Ghost Default' -Type default -Ghost + New-UDAntDesignButton -Text 'Ghost Dashed' -Type dashed -Ghost + ) + + Shows the transparent ghost treatment that is useful on stronger or more colorful backgrounds. + + .EXAMPLE + # Danger buttons + @( + New-UDAntDesignButton -Text 'Delete Record' -Type primary -Danger + New-UDAntDesignButton -Text 'Danger Default' -Type default -Danger + New-UDAntDesignButton -Text 'Danger Text' -Type text -Danger + New-UDAntDesignButton -Text 'Danger Link' -Type link -Danger -Href 'https://ant.design/components/button/' + ) + + Applies the danger treatment for destructive or high-risk actions. + + .EXAMPLE + # Block button + @( + New-UDAntDesignButton -Text 'Full Width Primary' -Type primary -Block + New-UDAntDesignButton -Text 'Full Width Default' -Type default -Block + ) + + Expands buttons to the available width to match the block layout from the Ant Design docs. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [string]$Text, + [object]$Content, + [object]$Icon, + [ValidateSet('default', 'primary', 'dashed', 'link', 'text')] + [string]$Type, + [ValidateSet('default', 'primary', 'danger', 'blue', 'purple', 'cyan', 'green', 'magenta', 'pink', 'red', 'orange', 'yellow', 'volcano', 'geekblue', 'lime', 'gold')] + [string]$Color, + [ValidateSet('outlined', 'dashed', 'solid', 'filled', 'text', 'link')] + [string]$Variant, + [ValidateSet('default', 'circle', 'round')] + [string]$Shape, + [ValidateSet('small', 'middle', 'large')] + [string]$Size, + [switch]$Disabled, + [switch]$Loading, + [int]$LoadingDelay, + [object]$LoadingIcon, + [switch]$Ghost, + [switch]$Danger, + [switch]$Block, + [string]$Href, + [ValidateSet('submit', 'button', 'reset')] + [string]$HtmlType, + [bool]$AutoInsertSpace, + [ValidateSet('start', 'end')] + [string]$IconPosition, + [string]$ClassName, + [string]$RootClassName, + [hashtable]$DataAttributes, + [Endpoint]$OnClick, + [object]$Value + ) + + if (-not $PSBoundParameters.ContainsKey('Text') -and -not $PSBoundParameters.ContainsKey('Content')) { + throw 'New-UDAntDesignButton requires -Text or -Content.' + } + + if ($null -ne $OnClick -and $OnClick.PSObject.Methods.Name -contains 'Register') { + $OnClick.Register($Id, $PSCmdlet) + } + + $descriptor = @{ + type = 'antd-button' + id = $Id + } + + if ($PSBoundParameters.ContainsKey('Text')) { + $descriptor.text = $Text + } + + if ($PSBoundParameters.ContainsKey('Content')) { + $descriptor.content = $Content + } + + if ($PSBoundParameters.ContainsKey('Icon')) { + $descriptor.icon = $Icon + } + + if ($PSBoundParameters.ContainsKey('Type')) { + $descriptor.buttonType = $Type + } + + foreach ($property in 'Color', 'Variant', 'Shape', 'Size', 'Href', 'HtmlType', 'IconPosition', 'ClassName', 'RootClassName') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + foreach ($switchProperty in 'Disabled', 'Ghost', 'Danger', 'Block') { + if ($PSBoundParameters.ContainsKey($switchProperty)) { + $descriptor[$switchProperty.Substring(0, 1).ToLowerInvariant() + $switchProperty.Substring(1)] = [bool]$PSBoundParameters[$switchProperty] + } + } + + if ($PSBoundParameters.ContainsKey('AutoInsertSpace')) { + $descriptor.autoInsertSpace = $AutoInsertSpace + } + + if ($PSBoundParameters.ContainsKey('Loading') -or $PSBoundParameters.ContainsKey('LoadingDelay') -or $PSBoundParameters.ContainsKey('LoadingIcon')) { + if ($PSBoundParameters.ContainsKey('LoadingDelay') -or $PSBoundParameters.ContainsKey('LoadingIcon')) { + $loadingDescriptor = @{} + + if ($PSBoundParameters.ContainsKey('LoadingDelay')) { + $loadingDescriptor.delay = $LoadingDelay + } + + if ($PSBoundParameters.ContainsKey('LoadingIcon')) { + $loadingDescriptor.icon = $LoadingIcon + } + + $descriptor.loading = $loadingDescriptor + } + else { + $descriptor.loading = [bool]$Loading + } + } + elseif ($PSBoundParameters.ContainsKey('Loading')) { + $descriptor.loading = [bool]$Loading + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + if ($PSBoundParameters.ContainsKey('Value')) { + $descriptor.value = $Value + } + + if ($null -ne $OnClick) { + $descriptor.onClick = $OnClick + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Checkbox.ps1 b/Apps/Frameworks/AntDesign/Public/Checkbox.ps1 new file mode 100644 index 0000000..4506c6f --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Checkbox.ps1 @@ -0,0 +1,140 @@ +function New-UDAntDesignCheckbox { + <# + .SYNOPSIS + Creates an Ant Design checkbox descriptor. + + .DESCRIPTION + Creates an antd-checkbox descriptor that maps the PowerShell command surface to the Ant Design Checkbox component used by the client runtime. The command mirrors the core Checkbox API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks. + + .NOTES + Used for selecting multiple values from several options. + If you use only one checkbox, it is the same as using Switch to toggle between two states. The difference is that Switch will trigger the state change directly, but Checkbox just marks the state as changed and this needs to be submitted. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Label + Specifies the checkbox label content. Provide plain text or descriptor content. + + .PARAMETER Checked + Determines whether the checkbox is selected. + + .PARAMETER DefaultChecked + Specifies the initial selected state of the checkbox. + + .PARAMETER Disabled + Disables the checkbox. + + .PARAMETER Indeterminate + Displays the checkbox in the indeterminate state. + + .PARAMETER AutoFocus + Automatically focuses the checkbox when it is rendered. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design checkbox wrapper. + + .PARAMETER ClassNames + Specifies semantic DOM class mappings passed to the Ant Design checkbox classNames prop. + + .PARAMETER Styles + Specifies semantic DOM inline style mappings passed to the Ant Design checkbox styles prop. + + .PARAMETER Style + Specifies inline styles applied to the Ant Design checkbox wrapper. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered checkbox. Keys are emitted as data-* attributes. + + .PARAMETER OnChange + Specifies the endpoint invoked when the checked state changes. + + .PARAMETER Value + Specifies a value sent back through the change event payload. + + .EXAMPLE + # Basic + @( + New-UDAntDesignCheckbox -Label 'Remember me' + New-UDAntDesignCheckbox -Label 'Send status updates' -DefaultChecked $true + ) + + Demonstrates the default unchecked and checked Ant Design checkbox states. + + .EXAMPLE + # Disabled + @( + New-UDAntDesignCheckbox -Label 'Archived item' -Disabled + New-UDAntDesignCheckbox -Label 'Pinned item' -DefaultChecked $true -Disabled + ) + + Shows disabled checkbox states for unchecked and checked options. + + .EXAMPLE + # Indeterminate + New-UDAntDesignCheckbox -Label 'Partially selected permissions' -Indeterminate + + Renders the checkbox in the indeterminate state, which is useful for partial selection flows. + + .EXAMPLE + # Custom styling + New-UDAntDesignCheckbox -DefaultChecked $true -Label 'Styled option' -Style @{ + color = '#d46b08' + backgroundColor = '#fff7e6' + paddingInline = '12px' + paddingBlock = '6px' + borderRadius = '8px' + } + + Applies wrapper-level inline styling to the checkbox so the option can match surrounding content. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Label, + [bool]$Checked, + [bool]$DefaultChecked, + [switch]$Disabled, + [switch]$Indeterminate, + [switch]$AutoFocus, + [string]$ClassName, + [hashtable]$ClassNames, + [hashtable]$Styles, + [hashtable]$Style, + [hashtable]$DataAttributes, + [Endpoint]$OnChange, + [object]$Value + ) + + if ($null -ne $OnChange -and $OnChange.PSObject.Methods.Name -contains 'Register') { + $OnChange.Register($Id, $PSCmdlet) + } + + $descriptor = @{ + type = 'antd-checkbox' + id = $Id + } + + foreach ($property in 'Label', 'Checked', 'DefaultChecked', 'ClassName', 'ClassNames', 'Styles', 'Style', 'Value') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + foreach ($switchProperty in 'Disabled', 'Indeterminate', 'AutoFocus') { + if ($PSBoundParameters.ContainsKey($switchProperty)) { + $descriptor[$switchProperty.Substring(0, 1).ToLowerInvariant() + $switchProperty.Substring(1)] = [bool]$PSBoundParameters[$switchProperty] + } + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + if ($null -ne $OnChange) { + $descriptor.onChange = $OnChange + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Demo.ps1 b/Apps/Frameworks/AntDesign/Public/Demo.ps1 new file mode 100644 index 0000000..e5563ed --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Demo.ps1 @@ -0,0 +1,36 @@ +function New-AntDesignDemo { + [CmdletBinding()] + [OutputType([object[]])] + param() + + @{ + type = 'antd-docs' + id = 'antdesign-docs' + title = 'Ant Design Components' + overview = 'Component documentation for the PowerShell Universal Ant Design framework. The examples shown in the page are generated from the module command help so the docs and comment-based help stay aligned.' + components = @( + Get-AntDesignComponentDocumentation -Key 'button' -Title 'Button' -CommandName 'New-UDAntDesignButton' -SourceUrl 'https://ant.design/components/button/' + Get-AntDesignComponentDocumentation -Key 'checkbox' -Title 'Checkbox' -CommandName 'New-UDAntDesignCheckbox' -Category 'Data Entry' -SourceUrl 'https://ant.design/components/checkbox' + Get-AntDesignComponentDocumentation -Key 'grid' -Title 'Grid' -CommandName 'New-UDAntDesignRow' -Category 'Layout' -SourceUrl 'https://ant.design/components/grid/' + Get-AntDesignComponentDocumentation -Key 'input' -Title 'Input' -CommandName 'New-UDAntDesignInput' -Category 'Data Entry' -SourceUrl 'https://ant.design/components/input' + Get-AntDesignComponentDocumentation -Key 'layout' -Title 'Layout' -CommandName 'New-UDAntDesignLayout' -Category 'Layout' -SourceUrl 'https://ant.design/components/layout/' + Get-AntDesignComponentDocumentation -Key 'rate' -Title 'Rate' -CommandName 'New-UDAntDesignRate' -Category 'Data Entry' -SourceUrl 'https://ant.design/components/rate' + Get-AntDesignComponentDocumentation -Key 'switch' -Title 'Switch' -CommandName 'New-UDAntDesignSwitch' -Category 'Data Entry' -SourceUrl 'https://ant.design/components/switch' + Get-AntDesignComponentDocumentation -Key 'typography' -Title 'Typography' -CommandName 'New-UDAntDesignTypography' -SourceUrl 'https://ant.design/components/typography' + Get-AntDesignComponentDocumentation -Key 'message' -Title 'Message' -CommandName 'Show-AntDesignMessage' -Category 'Feedback' -SourceUrl 'https://ant.design/components/message/' + ) + } +} + +function New-AntDesignDemoApp { + [CmdletBinding()] + param() + + if (-not (Get-Command -Name 'New-UDApp' -ErrorAction Ignore)) { + throw 'New-AntDesignDemoApp requires PowerShell Universal and the Universal cmdlets to be loaded.' + } + + New-UDApp -Title 'Ant Design Components' -Content { + New-AntDesignDemo + } +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Framework.ps1 b/Apps/Frameworks/AntDesign/Public/Framework.ps1 new file mode 100644 index 0000000..4e33021 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Framework.ps1 @@ -0,0 +1,58 @@ +function Get-PSUAntDesignFrameworkAssetBasePath { + [CmdletBinding()] + [OutputType([string])] + param() + + '/frameworks/ant-design' +} + +function Get-PSUAntDesignFrameworkManifestPath { + [CmdletBinding()] + [OutputType([string])] + param() + + $moduleRoot = Split-Path -Path $PSScriptRoot -Parent + Join-Path -Path $moduleRoot -ChildPath 'dist\manifest.json' +} + +function Get-PSUAntDesignFrameworkManifestEntry { + [CmdletBinding()] + [OutputType([hashtable])] + param() + + $manifestPath = Get-PSUAntDesignFrameworkManifestPath + + if (-not (Test-Path -Path $manifestPath)) { + throw "Ant Design framework manifest not found at '$manifestPath'. Run the frontend build first." + } + + $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json -AsHashtable + $entry = $manifest['index.html'] + + if (-not $entry) { + throw "Ant Design framework manifest entry for 'index.html' was not found in '$manifestPath'." + } + + $entry +} + +function Get-PSUAntDesignFrameworkEntryPoint { + [CmdletBinding()] + [OutputType([pscustomobject])] + param() + + $basePath = Get-PSUAntDesignFrameworkAssetBasePath + $entry = Get-PSUAntDesignFrameworkManifestEntry + $scriptPath = $entry.file -replace '\\', '/' + $stylesheetPath = $null + + if ($entry.ContainsKey('css') -and $entry.css.Count -gt 0) { + $stylesheetPath = $entry.css[0] -replace '\\', '/' + } + + [pscustomobject]@{ + BasePath = $basePath + ScriptPath = "$basePath/$scriptPath" + StylesheetPath = if ($stylesheetPath) { "$basePath/$stylesheetPath" } else { $null } + } +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Grid.ps1 b/Apps/Frameworks/AntDesign/Public/Grid.ps1 new file mode 100644 index 0000000..5764a8b --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Grid.ps1 @@ -0,0 +1,250 @@ +function New-UDAntDesignRow { + <# + .SYNOPSIS + Creates an Ant Design grid row descriptor. + + .DESCRIPTION + Creates an antd-row descriptor that maps the PowerShell command surface to the Ant Design Row component used by the client runtime. Use grid rows as the outer layout container for Ant Design grid columns so dashboard content can be arranged in the standard 24-column grid system. + + .NOTES + Use rows to define horizontal layout bands and place only Ant Design grid columns directly inside the row content. + Grid rows support horizontal and vertical gutters, flex alignment, and wrapping. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Content + Specifies the descriptor content rendered inside the row. This should generally contain one or more Ant Design grid columns. + + .PARAMETER Align + Specifies the vertical alignment of columns within the row. + + .PARAMETER Justify + Specifies the horizontal alignment and distribution of columns within the row. + + .PARAMETER Gutter + Specifies spacing between columns. Provide a number, a responsive hashtable such as `@{ xs = 8; md = 24 }`, or a two-item array for horizontal and vertical spacing. + + .PARAMETER Wrap + Controls whether columns wrap onto additional lines when they exceed the available width. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design row element. + + .PARAMETER Style + Specifies inline styles applied to the Ant Design row element. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered row. Keys are emitted as data-* attributes. + + .EXAMPLE + # Basic grid + New-UDAntDesignRow -Gutter 16 -Content @( + New-UDAntDesignCol -Span 12 -Content (New-UDAntDesignText -Text 'col-12') + New-UDAntDesignCol -Span 12 -Content (New-UDAntDesignText -Text 'col-12') + ) + + Creates a simple two-column row using the Ant Design 24-column grid system. + + .EXAMPLE + # Mixed spans + New-UDAntDesignRow -Gutter 16 -Content @( + New-UDAntDesignCol -Span 8 -Content (New-UDAntDesignText -Text 'col-8') + New-UDAntDesignCol -Span 8 -Content (New-UDAntDesignText -Text 'col-8') + New-UDAntDesignCol -Span 8 -Content (New-UDAntDesignText -Text 'col-8') + ) + + Splits the row into three equal-width columns. + + .EXAMPLE + # Responsive gutter + New-UDAntDesignRow -Gutter @(@{ xs = 8; sm = 16; md = 24 }, 16) -Content @( + New-UDAntDesignCol -Span 12 -Content (New-UDAntDesignText -Text 'Responsive gutter') + New-UDAntDesignCol -Span 12 -Content (New-UDAntDesignText -Text 'Horizontal and vertical spacing') + ) + + Uses responsive horizontal gutter values with a fixed vertical gutter. + + .EXAMPLE + # Justify and align + New-UDAntDesignRow -Justify space-between -Align middle -Style @{ minHeight = '96px'; border = '1px dashed #d9d9d9'; paddingInline = '12px' } -Content @( + New-UDAntDesignCol -Span 4 -Content (New-UDAntDesignText -Text 'Left') + New-UDAntDesignCol -Span 4 -Content (New-UDAntDesignText -Text 'Center') + New-UDAntDesignCol -Span 4 -Content (New-UDAntDesignText -Text 'Right') + ) + + Demonstrates row-level flex alignment and distribution. + + .EXAMPLE + # Offset and order + New-UDAntDesignRow -Gutter 16 -Content @( + New-UDAntDesignCol -Span 6 -Order 2 -Content (New-UDAntDesignText -Text 'Second in visual order') + New-UDAntDesignCol -Span 6 -Offset 6 -Order 1 -Content (New-UDAntDesignText -Text 'First with offset') + ) + + Shows how column order and offset can reshape a row layout. + + .EXAMPLE + # Flex fill + New-UDAntDesignRow -Gutter 16 -Wrap:$false -Content @( + New-UDAntDesignCol -Flex '100px' -Content (New-UDAntDesignText -Text '100px') + New-UDAntDesignCol -Flex auto -Content (New-UDAntDesignText -Text 'Auto width') + New-UDAntDesignCol -Flex '1 1 240px' -Content (New-UDAntDesignText -Text 'Flexible remainder') + ) + + Uses column flex values instead of span-only sizing. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [ValidateSet('top', 'middle', 'bottom', 'stretch')] + [string]$Align, + [ValidateSet('start', 'end', 'center', 'space-around', 'space-between', 'space-evenly')] + [string]$Justify, + [object]$Gutter, + [bool]$Wrap, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + $descriptor = @{ + type = 'antd-row' + id = $Id + } + + foreach ($property in 'Content', 'Align', 'Justify', 'Gutter', 'Wrap', 'ClassName', 'Style') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + $descriptor +} + +function New-UDAntDesignCol { + <# + .SYNOPSIS + Creates an Ant Design grid column descriptor. + + .DESCRIPTION + Creates an antd-col descriptor that maps the PowerShell command surface to the Ant Design Col component used by the client runtime. Use grid columns inside Ant Design rows to size and position dashboard content across the 24-column layout system. + + .NOTES + Grid columns should be rendered inside Ant Design grid rows. + Columns support fixed spans, offsets, ordering, flex sizing, and responsive breakpoint-specific settings. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Content + Specifies the descriptor content rendered inside the column. + + .PARAMETER Span + Specifies the number of grid columns occupied by this column. + + .PARAMETER Order + Specifies the display order when the row uses flex layout. + + .PARAMETER Offset + Specifies how many grid columns to shift this column to the right. + + .PARAMETER Push + Specifies how many grid columns to push this column to the right. + + .PARAMETER Pull + Specifies how many grid columns to pull this column to the left. + + .PARAMETER Flex + Specifies the flex sizing value for the column. + + .PARAMETER Xs + Specifies the responsive configuration for viewports smaller than 576 pixels. + + .PARAMETER Sm + Specifies the responsive configuration for viewports at least 576 pixels wide. + + .PARAMETER Md + Specifies the responsive configuration for viewports at least 768 pixels wide. + + .PARAMETER Lg + Specifies the responsive configuration for viewports at least 992 pixels wide. + + .PARAMETER Xl + Specifies the responsive configuration for viewports at least 1200 pixels wide. + + .PARAMETER Xxl + Specifies the responsive configuration for viewports at least 1600 pixels wide. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design column element. + + .PARAMETER Style + Specifies inline styles applied to the Ant Design column element. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered column. Keys are emitted as data-* attributes. + + .EXAMPLE + # Basic column + New-UDAntDesignCol -Span 6 -Content (New-UDAntDesignText -Text 'col-6') + + Creates a simple 6-span grid column. + + .EXAMPLE + # Responsive span + New-UDAntDesignCol -Xs 24 -Md @{ span = 12; offset = 6 } -Content (New-UDAntDesignText -Text 'Responsive column') + + Uses a full-width mobile layout and a centered medium breakpoint layout. + + .EXAMPLE + # Flexible width + New-UDAntDesignCol -Flex '1 1 240px' -Content (New-UDAntDesignText -Text 'Flexible column') + + Uses CSS flex sizing instead of a fixed span. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [object]$Span, + [object]$Order, + [object]$Offset, + [object]$Push, + [object]$Pull, + [object]$Flex, + [object]$Xs, + [object]$Sm, + [object]$Md, + [object]$Lg, + [object]$Xl, + [object]$Xxl, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + $descriptor = @{ + type = 'antd-col' + id = $Id + } + + foreach ($property in 'Content', 'Span', 'Order', 'Offset', 'Push', 'Pull', 'Flex', 'Xs', 'Sm', 'Md', 'Lg', 'Xl', 'Xxl', 'ClassName', 'Style') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Input.ps1 b/Apps/Frameworks/AntDesign/Public/Input.ps1 new file mode 100644 index 0000000..7b6a4f2 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Input.ps1 @@ -0,0 +1,418 @@ +function New-UDAntDesignInput { + <# + .SYNOPSIS + Creates an Ant Design input descriptor. + + .DESCRIPTION + Creates an antd-input descriptor that maps the PowerShell command surface to the Ant Design Input family used by the client runtime. The command wraps Input, Input.TextArea, Input.Search, Input.Password, and Input.OTP behind a single descriptor model so the documented PowerShell examples can follow the same examples shown in the upstream Ant Design Input documentation while still supporting PowerShell Universal endpoint callbacks. + + .NOTES + A user input in a form field is needed. + A search input is required. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Mode + Specifies which Ant Design Input primitive to render. Valid values are input, textarea, search, password, and otp. + + .PARAMETER Placeholder + Specifies the placeholder text rendered by the input. + + .PARAMETER Value + Specifies the controlled value rendered by the input. + + .PARAMETER DefaultValue + Specifies the initial value rendered by the input. + + .PARAMETER Disabled + Disables the input. + + .PARAMETER AllowClear + Displays the Ant Design clear affordance when the input mode supports it. + + .PARAMETER AutoFocus + Automatically focuses the input when it is rendered. + + .PARAMETER AutoComplete + Specifies the browser autocomplete behavior. + + .PARAMETER Type + Specifies the native input type used by the base input control. + + .PARAMETER Size + Specifies the Ant Design input size. Valid values are large, middle, and small. + + .PARAMETER Variant + Specifies the Ant Design input variant. Valid values are outlined, filled, borderless, and underlined. + + .PARAMETER Status + Specifies the validation status. Valid values are error and warning. + + .PARAMETER Prefix + Specifies content rendered before the input value. Provide plain text, descriptor content, or an Ant Design icon name. + + .PARAMETER Suffix + Specifies content rendered after the input value. Provide plain text, descriptor content, or an Ant Design icon name. + + .PARAMETER AddonBefore + Specifies content rendered before the outer input wrapper. + + .PARAMETER AddonAfter + Specifies content rendered after the outer input wrapper. + + .PARAMETER EnterButton + Specifies the Ant Design search enter button. Provide `$true`, plain text, or descriptor content. + + .PARAMETER Loading + Shows the Ant Design loading state for search input mode. + + .PARAMETER Rows + Specifies the visible row count for textarea mode. + + .PARAMETER AutoSize + Enables Ant Design textarea auto-sizing. Provide `$true` or a hashtable with `minRows` and `maxRows`. + + .PARAMETER MaxLength + Specifies the maximum character length. + + .PARAMETER ShowCount + Displays the Ant Design character count affordance. + + .PARAMETER Count + Specifies count configuration for Input. Supported keys are `show`, `max`, `strategy`, and `exceedFormatter`. Use `strategy = 'graphemes'` and `exceedFormatter = 'truncate-graphemes'` to mirror the documented emoji counting examples. + + .PARAMETER OtpLength + Specifies the number of OTP input slots rendered in otp mode. + + .PARAMETER OtpMask + Specifies the OTP mask. Provide `$true` or a single display character. + + .PARAMETER OtpFormatter + Specifies a built-in OTP formatter. Valid values are uppercase. + + .PARAMETER OtpSeparator + Specifies the separator rendered between OTP fields. Provide plain text, descriptor content, or a hashtable like `@{ type = 'alternating-dash'; evenColor = 'red'; oddColor = 'blue' }`. + + .PARAMETER PasswordVisible + Specifies the controlled visibility state used by password mode. + + .PARAMETER VisibilityToggle + Specifies whether the password visibility toggle is shown. + + .PARAMETER Name + Specifies the name attribute rendered by the input. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design input element or wrapper. + + .PARAMETER ClassNames + Specifies semantic DOM class mappings passed to the Ant Design input `classNames` prop when the rendered mode supports it. + + .PARAMETER Style + Specifies inline styles applied to the rendered Ant Design input element or wrapper. + + .PARAMETER Styles + Specifies semantic DOM inline style mappings passed to the Ant Design input `styles` prop when the rendered mode supports it. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered input. Keys are emitted as data-* attributes. + + .PARAMETER OnChange + Specifies the endpoint invoked when the input value changes. + + .PARAMETER OnPressEnter + Specifies the endpoint invoked when Enter is pressed inside the input. + + .PARAMETER OnSearch + Specifies the endpoint invoked when search input mode triggers a search. + + .PARAMETER OnClear + Specifies the endpoint invoked when the clear affordance is used. + + .PARAMETER OnInput + Specifies the endpoint invoked when otp mode emits its input array payload. + + .EXAMPLE + # Basic usage + New-UDAntDesignInput -Placeholder 'Basic usage' + + Basic usage example. + + .EXAMPLE + # Three sizes of Input + @( + New-UDAntDesignInput -Size large -Placeholder 'large size' -Prefix 'UserOutlined' + New-UDAntDesignInput -Placeholder 'default size' -Prefix 'UserOutlined' + New-UDAntDesignInput -Size small -Placeholder 'small size' -Prefix 'UserOutlined' + ) + + There are three sizes of an Input box: `large` (40px), `medium` (32px) and `small` (24px). + + .EXAMPLE + # Variants + @( + New-UDAntDesignInput -Placeholder 'Outlined' + New-UDAntDesignInput -Placeholder 'Filled' -Variant filled + New-UDAntDesignInput -Placeholder 'Borderless' -Variant borderless + New-UDAntDesignInput -Placeholder 'Underlined' -Variant underlined + New-UDAntDesignInput -Mode search -Placeholder 'Filled search' -Variant filled + ) + + Variants of Input, there are four variants: `outlined` `filled` `borderless` and `underlined`. + + .EXAMPLE + # Compact Style + @( + New-UDAntDesignInput -DefaultValue '26888888' + New-UDAntDesignInput -DefaultValue '0571' -Style @{ width = '20%' } + New-UDAntDesignInput -DefaultValue '26888888' -Style @{ width = '80%' } + New-UDAntDesignInput -Mode search -AddonBefore 'https://' -Placeholder 'input search text' -AllowClear + New-UDAntDesignInput -DefaultValue 'Combine input and button' -AddonAfter (New-UDAntDesignButton -Text 'Submit' -ButtonType primary) + New-UDAntDesignInput -AddonBefore 'Zhejiang' -DefaultValue 'Xihu District, Hangzhou' + ) + + Uses add-ons, search affordances, and paired values to reproduce the compact input arrangements shown in the Ant Design docs. + + .EXAMPLE + # Search box + @( + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -Style @{ width = 200 } + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -AllowClear -Style @{ width = 200 } + New-UDAntDesignInput -Mode search -AddonBefore 'https://' -Placeholder 'input search text' -AllowClear + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -EnterButton $true + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -AllowClear -EnterButton 'Search' -Size large + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -EnterButton 'Search' -Size large -Suffix 'AudioOutlined' + ) + + Example of creating a search box by grouping a standard input with a search button. + + .EXAMPLE + # Search box with loading + @( + New-UDAntDesignInput -Mode search -Placeholder 'input search loading default' -Loading + New-UDAntDesignInput -Mode search -Placeholder 'input search loading with enterButton' -Loading -EnterButton $true + New-UDAntDesignInput -Mode search -Placeholder 'input search text' -EnterButton 'Search' -Size large -Loading + ) + + Search loading when onSearch. + + .EXAMPLE + # TextArea + @( + New-UDAntDesignInput -Mode textarea -Rows 4 + New-UDAntDesignInput -Mode textarea -Rows 4 -Placeholder 'maxLength is 6' -MaxLength 6 + ) + + For multi-line input. + + .EXAMPLE + # Autosizing the height to fit the content + @( + New-UDAntDesignInput -Mode textarea -Placeholder 'Autosize height based on content lines' -AutoSize $true + New-UDAntDesignInput -Mode textarea -Placeholder 'Autosize height with minimum and maximum number of lines' -AutoSize @{ minRows = 2; maxRows = 6 } + New-UDAntDesignInput -Mode textarea -Value 'Controlled autosize' -Placeholder 'Controlled autosize' -AutoSize @{ minRows = 3; maxRows = 5 } + ) + + `autoSize` for a textarea input automatically adjusts height based on the content. You can also supply `minRows` and `maxRows`. + + .EXAMPLE + # OTP + @( + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With formatter (Upcase)' + New-UDAntDesignInput -Mode otp -OtpFormatter uppercase + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With Disabled' + New-UDAntDesignInput -Mode otp -Disabled + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With Length (8)' + New-UDAntDesignInput -Mode otp -OtpLength 8 + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With variant' + New-UDAntDesignInput -Mode otp -Variant filled + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With custom display character' + New-UDAntDesignInput -Mode otp -OtpMask '🔒' + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With custom ReactNode separator' + New-UDAntDesignInput -Mode otp -OtpSeparator '/' + New-UDAntDesignTypography -Kind title -Level 5 -Text 'With custom function separator' + New-UDAntDesignInput -Mode otp -OtpSeparator @{ type = 'alternating-dash'; evenColor = 'red'; oddColor = 'blue' } + ) + + One time password input. + + .EXAMPLE + # Format Tooltip Input + New-UDAntDesignInput -Placeholder 'Input a number' -MaxLength 16 -Style @{ width = 120 } -Suffix 'InfoCircleOutlined' + + You can use Input alongside contextual hints to create a numeric input pattern that keeps extra-long values readable. + + .EXAMPLE + # prefix and suffix + @( + New-UDAntDesignInput -Placeholder 'Enter your username' -Prefix 'UserOutlined' -Suffix 'InfoCircleOutlined' + New-UDAntDesignInput -Prefix '¥' -Suffix 'RMB' + New-UDAntDesignInput -Prefix '¥' -Suffix 'RMB' -Disabled + New-UDAntDesignInput -Mode password -Suffix 'LockOutlined' -Placeholder 'input password support suffix' + ) + + Add a prefix or suffix icon or label inside the input. + + .EXAMPLE + # Password box + @( + New-UDAntDesignInput -Mode password -Placeholder 'input password' + New-UDAntDesignInput -Mode password -Placeholder 'input password' -PasswordVisible $true + New-UDAntDesignInput -Mode password -Placeholder 'disabled input password' -Disabled + ) + + Input type of password. + + .EXAMPLE + # With clear icon + @( + New-UDAntDesignInput -Placeholder 'input with clear icon' -AllowClear + New-UDAntDesignInput -Mode textarea -Placeholder 'textarea with clear icon' -AllowClear + ) + + Input box with the remove icon, click the icon to delete everything. + + .EXAMPLE + # With character counting + @( + New-UDAntDesignInput -ShowCount -MaxLength 20 + New-UDAntDesignInput -Mode textarea -ShowCount -MaxLength 100 -Placeholder 'can resize' + New-UDAntDesignInput -Mode textarea -ShowCount -MaxLength 100 -Placeholder 'disable resize' -Style @{ height = 120; resize = 'none' } + ) + + Show character counting. + + .EXAMPLE + # Custom count logic + @( + New-UDAntDesignTypography -Kind title -Level 5 -Text 'Exceed Max' + New-UDAntDesignInput -Count @{ show = $true; max = 10 } -DefaultValue 'Hello, antd!' + New-UDAntDesignTypography -Kind title -Level 5 -Text 'Emoji count as length 1' + New-UDAntDesignInput -Count @{ show = $true; strategy = 'graphemes' } -DefaultValue '🔥🔥🔥' + New-UDAntDesignTypography -Kind title -Level 5 -Text 'Not exceed max' + New-UDAntDesignInput -Count @{ show = $true; max = 6; strategy = 'graphemes'; exceedFormatter = 'truncate-graphemes' } -DefaultValue '🔥 antd' + ) + + Customize the counting strategy in scenarios such as emoji-aware length counting by using the `count` configuration. + + .EXAMPLE + # Status + @( + New-UDAntDesignInput -Status error -Placeholder 'Error' + New-UDAntDesignInput -Status warning -Placeholder 'Warning' + New-UDAntDesignInput -Status error -Prefix 'ClockCircleOutlined' -Placeholder 'Error with prefix' + New-UDAntDesignInput -Status warning -Prefix 'ClockCircleOutlined' -Placeholder 'Warning with prefix' + ) + + Add status to Input with `status`, which could be `error` or `warning`. + + .EXAMPLE + # Focus + @( + New-UDAntDesignTypography -Kind paragraph -Text 'Input focus methods are available on the underlying Ant Design controls.' + New-UDAntDesignInput -DefaultValue 'Ant Design love you!' -AutoFocus + New-UDAntDesignInput -Mode textarea -DefaultValue 'Ant Design love you!' + ) + + Focus with additional option. + + .EXAMPLE + # Custom semantic dom styling + @( + New-UDAntDesignInput -ClassNames @{ root = 'input-example-root' } -Styles @{ root = @{ borderColor = '#696FC7' } } -Placeholder 'Object' + New-UDAntDesignInput -Placeholder 'Function-inspired styling' -Styles @{ root = @{ borderColor = '#696FC7' } } -Size middle + New-UDAntDesignInput -Mode textarea -Value 'TextArea' -ShowCount -Styles @{ root = @{ borderColor = '#BDE3C3' }; textarea = @{ resize = 'none' }; count = @{ color = '#BDE3C3' } } + New-UDAntDesignInput -Mode password -Value 'Password' -Styles @{ root = @{ borderColor = '#F5D3C4' } } -Size middle + New-UDAntDesignInput -Mode otp -OtpLength 6 -OtpSeparator '*' + New-UDAntDesignInput -Mode search -Placeholder 'Search' -Size large -Styles @{ root = @{ color = '#4DA8DA' }; input = @{ color = '#4DA8DA'; borderColor = '#4DA8DA' }; prefix = @{ color = '#4DA8DA' }; suffix = @{ color = '#4DA8DA' }; count = @{ color = '#4DA8DA' } } + ) + + You can customize the semantic DOM styling of Input by passing objects through `classNames` and `styles`. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [ValidateSet('input', 'textarea', 'search', 'password', 'otp')] + [string]$Mode = 'input', + [string]$Placeholder, + [string]$Value, + [string]$DefaultValue, + [switch]$Disabled, + [switch]$AllowClear, + [switch]$AutoFocus, + [string]$AutoComplete, + [string]$Type, + [ValidateSet('large', 'middle', 'small')] + [string]$Size, + [ValidateSet('outlined', 'filled', 'borderless', 'underlined')] + [string]$Variant, + [ValidateSet('error', 'warning')] + [string]$Status, + [object]$Prefix, + [object]$Suffix, + [object]$AddonBefore, + [object]$AddonAfter, + [object]$EnterButton, + [switch]$Loading, + [int]$Rows, + [object]$AutoSize, + [int]$MaxLength, + [switch]$ShowCount, + [hashtable]$Count, + [int]$OtpLength, + [object]$OtpMask, + [ValidateSet('uppercase')] + [string]$OtpFormatter, + [object]$OtpSeparator, + [bool]$PasswordVisible, + [object]$VisibilityToggle, + [string]$Name, + [string]$ClassName, + [hashtable]$ClassNames, + [hashtable]$Style, + [hashtable]$Styles, + [hashtable]$DataAttributes, + [Endpoint]$OnChange, + [Endpoint]$OnPressEnter, + [Endpoint]$OnSearch, + [Endpoint]$OnClear, + [Endpoint]$OnInput + ) + + foreach ($endpoint in @($OnChange, $OnPressEnter, $OnSearch, $OnClear, $OnInput)) { + if ($null -ne $endpoint -and $endpoint.PSObject.Methods.Name -contains 'Register') { + $endpoint.Register($Id, $PSCmdlet) + } + } + + $descriptor = @{ + type = 'antd-input' + id = $Id + } + + foreach ($property in 'Mode', 'Placeholder', 'Value', 'DefaultValue', 'AutoComplete', 'Type', 'Size', 'Variant', 'Status', 'Prefix', 'Suffix', 'AddonBefore', 'AddonAfter', 'EnterButton', 'Rows', 'AutoSize', 'MaxLength', 'Count', 'OtpLength', 'OtpMask', 'OtpFormatter', 'OtpSeparator', 'PasswordVisible', 'VisibilityToggle', 'Name', 'ClassName', 'ClassNames', 'Style', 'Styles') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + foreach ($switchProperty in 'Disabled', 'AllowClear', 'AutoFocus', 'Loading', 'ShowCount') { + if ($PSBoundParameters.ContainsKey($switchProperty)) { + $descriptor[$switchProperty.Substring(0, 1).ToLowerInvariant() + $switchProperty.Substring(1)] = [bool]$PSBoundParameters[$switchProperty] + } + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + foreach ($eventName in 'OnChange', 'OnPressEnter', 'OnSearch', 'OnClear', 'OnInput') { + if ($PSBoundParameters.ContainsKey($eventName)) { + $descriptor[$eventName.Substring(0, 1).ToLowerInvariant() + $eventName.Substring(1)] = $PSBoundParameters[$eventName] + } + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Layout.ps1 b/Apps/Frameworks/AntDesign/Public/Layout.ps1 new file mode 100644 index 0000000..aaff353 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Layout.ps1 @@ -0,0 +1,221 @@ +function New-AntDesignLayoutDescriptor { + param( + [Parameter(Mandatory)] + [string]$Type, + [Parameter(Mandatory)] + [string]$Id, + [object]$Content, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes, + [hashtable]$AdditionalProperties = @{} + ) + + $descriptor = @{ + type = $Type + id = $Id + } + + if ($PSBoundParameters.ContainsKey('Content')) { + $descriptor.content = $Content + } + + if ($PSBoundParameters.ContainsKey('ClassName')) { + $descriptor.className = $ClassName + } + + if ($PSBoundParameters.ContainsKey('Style')) { + $descriptor.style = $Style + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + foreach ($key in $AdditionalProperties.Keys) { + $descriptor[$key] = $AdditionalProperties[$key] + } + + $descriptor +} + +function New-UDAntDesignLayout { + <# + .SYNOPSIS + Creates an Ant Design layout descriptor. + + .DESCRIPTION + Creates an antd-layout descriptor that maps the PowerShell command surface to the Ant Design Layout component used by the client runtime. Use the layout wrapper to compose page chrome with Header, Sider, Content, and Footer regions while keeping the descriptor contract aligned with the upstream Ant Design layout model. + + .NOTES + Layout is the outer container for page structure and can contain nested layouts when a page needs both top and side navigation. + Use Header for top navigation or branding, Sider for navigation rails, Content for the main work area, and Footer for supporting information. + Set HasSider when you want to make the presence of a nested sider explicit, which can help avoid SSR layout flicker. + Sider supports collapsible and responsive behavior through the Ant Design built-in props exposed by the PowerShell wrapper. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Content + Specifies the descriptor content rendered inside the layout. This can contain Header, Sider, Content, Footer, or nested Layout descriptors. + + .PARAMETER HasSider + Indicates that the layout contains a sider. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design layout element. + + .PARAMETER Style + Specifies inline styles applied to the rendered layout element. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered layout. Keys are emitted as data-* attributes. + + .EXAMPLE + # Basic structure + New-UDAntDesignLayout -Style @{ minHeight = '280px'; border = '1px solid #f0f0f0'; borderRadius = '8px'; overflow = 'hidden' } -Content @( + New-UDAntDesignLayoutHeader -Style @{ background = '#1677ff'; color = '#fff'; padding = '0 24px'; display = 'flex'; alignItems = 'center' } -Content 'Header' + New-UDAntDesignLayoutContent -Style @{ background = '#fff'; padding = '24px' } -Content 'Content' + New-UDAntDesignLayoutFooter -Style @{ textAlign = 'center' } -Content 'Footer' + ) + + Creates the classic header-content-footer page structure. + + .EXAMPLE + # Header and sider + New-UDAntDesignLayout -Style @{ minHeight = '320px'; border = '1px solid #f0f0f0'; borderRadius = '8px'; overflow = 'hidden' } -Content @( + New-UDAntDesignLayoutHeader -Style @{ background = '#001529'; color = '#fff'; padding = '0 24px'; display = 'flex'; alignItems = 'center' } -Content 'Header' + New-UDAntDesignLayout -HasSider $true -Content @( + New-UDAntDesignLayoutSider -Width 200 -Style @{ background = '#001529'; color = '#fff'; padding = '24px 16px' } -Content 'Sider' + New-UDAntDesignLayoutContent -Style @{ background = '#fff'; padding = '24px' } -Content 'Content' + ) + ) + + Nests a layout inside the root so a top header and left navigation rail can be combined. + + .EXAMPLE + # Sider content footer + New-UDAntDesignLayout -HasSider $true -Style @{ minHeight = '320px'; border = '1px solid #f0f0f0'; borderRadius = '8px'; overflow = 'hidden' } -Content @( + New-UDAntDesignLayoutSider -Width '220px' -Theme light -Style @{ padding = '24px 16px'; borderInlineEnd = '1px solid #f0f0f0' } -Content 'Navigation' + New-UDAntDesignLayout -Content @( + New-UDAntDesignLayoutContent -Style @{ background = '#fff'; padding = '24px' } -Content 'Work area' + New-UDAntDesignLayoutFooter -Style @{ textAlign = 'center'; background = '#fafafa' } -Content 'Footer' + ) + ) + + Uses a fixed navigation sider with a nested content-and-footer layout. + + .EXAMPLE + # Collapsible sider + New-UDAntDesignLayout -HasSider $true -Style @{ minHeight = '320px'; border = '1px solid #f0f0f0'; borderRadius = '8px'; overflow = 'hidden' } -Content @( + New-UDAntDesignLayoutSider -Collapsible $true -DefaultCollapsed $true -CollapsedWidth 80 -Width 220 -Style @{ paddingTop = '24px' } -Content 'Collapsible sider' + New-UDAntDesignLayoutContent -Style @{ background = '#fff'; padding = '24px' } -Content 'Content with collapsible navigation' + ) + + Enables the Ant Design built-in collapsible sider behavior without adding custom event handling. + + .EXAMPLE + # Responsive sider + New-UDAntDesignLayout -HasSider $true -Style @{ minHeight = '320px'; border = '1px solid #f0f0f0'; borderRadius = '8px'; overflow = 'hidden' } -Content @( + New-UDAntDesignLayoutSider -Breakpoint lg -CollapsedWidth 0 -Width 220 -Style @{ paddingTop = '24px' } -Content 'Responsive sider' + New-UDAntDesignLayoutContent -Style @{ background = '#fff'; padding = '24px' } -Content 'Resize the page to let the sider collapse at the large breakpoint.' + ) + + Shows the responsive breakpoint support exposed by the sider wrapper. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [bool]$HasSider, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + $additionalProperties = @{} + + if ($PSBoundParameters.ContainsKey('HasSider')) { + $additionalProperties.hasSider = $HasSider + } + + New-AntDesignLayoutDescriptor -Type 'antd-layout' -Id $Id -Content $Content -ClassName $ClassName -Style $Style -DataAttributes $DataAttributes -AdditionalProperties $additionalProperties +} + +function New-UDAntDesignLayoutHeader { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + New-AntDesignLayoutDescriptor -Type 'antd-layout-header' -Id $Id -Content $Content -ClassName $ClassName -Style $Style -DataAttributes $DataAttributes +} + +function New-UDAntDesignLayoutContent { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + New-AntDesignLayoutDescriptor -Type 'antd-layout-content' -Id $Id -Content $Content -ClassName $ClassName -Style $Style -DataAttributes $DataAttributes +} + +function New-UDAntDesignLayoutFooter { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + New-AntDesignLayoutDescriptor -Type 'antd-layout-footer' -Id $Id -Content $Content -ClassName $ClassName -Style $Style -DataAttributes $DataAttributes +} + +function New-UDAntDesignLayoutSider { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [object]$Content, + [ValidateSet('xs', 'sm', 'md', 'lg', 'xl', 'xxl', 'xxxl')] + [string]$Breakpoint, + [bool]$Collapsed, + [object]$CollapsedWidth, + [bool]$Collapsible, + [bool]$DefaultCollapsed, + [bool]$ReverseArrow, + [ValidateSet('light', 'dark')] + [string]$Theme, + [AllowNull()] + [object]$Trigger, + [object]$Width, + [hashtable]$ZeroWidthTriggerStyle, + [string]$ClassName, + [hashtable]$Style, + [hashtable]$DataAttributes + ) + + $additionalProperties = @{} + + foreach ($property in 'Breakpoint', 'Collapsed', 'CollapsedWidth', 'Collapsible', 'DefaultCollapsed', 'ReverseArrow', 'Theme', 'Trigger', 'Width', 'ZeroWidthTriggerStyle') { + if ($PSBoundParameters.ContainsKey($property)) { + $additionalProperties[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + New-AntDesignLayoutDescriptor -Type 'antd-layout-sider' -Id $Id -Content $Content -ClassName $ClassName -Style $Style -DataAttributes $DataAttributes -AdditionalProperties $additionalProperties +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Message.ps1 b/Apps/Frameworks/AntDesign/Public/Message.ps1 new file mode 100644 index 0000000..2c5ba4b --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Message.ps1 @@ -0,0 +1,84 @@ +function Show-AntDesignMessage { + <# + .SYNOPSIS + Shows an Ant Design global message in the current dashboard session. + + .DESCRIPTION + Sends a dashboard websocket message through the built-in DashboardHub variable so the Ant Design client runtime can display a global message. Use this for lightweight feedback after an action completes or while work is in progress. + + .NOTES + Use messages for short, non-blocking feedback such as success, warning, error, info, and loading states. + When invoked inside a dashboard endpoint, the current ConnectionId is used automatically when available. + Use -Broadcast to send the message to every connected client for the current dashboard. + + .PARAMETER Content + Specifies the message content rendered by the Ant Design Message component. + + .PARAMETER Type + Specifies the Ant Design message type. + + .PARAMETER Duration + Specifies how long the message should stay visible, in seconds. Use 0 to keep it open until it is replaced or destroyed by a later update. + + .PARAMETER Key + Specifies a stable key so later calls can update the same message instance. + + .PARAMETER Broadcast + Sends the message to all connected clients for the current dashboard. + + .EXAMPLE + New-UDAntDesignButton -Text 'Save Changes' -Type primary -OnClick { + Show-AntDesignMessage -Content 'Saved changes.' -Type success + } + + Displays a success message from a button click inside the current dashboard session. + + .EXAMPLE + New-UDAntDesignButton -Text 'Start Sync' -OnClick { + Show-AntDesignMessage -Content 'Sync in progress...' -Type loading -Duration 0 -Key 'sync-status' + } + + Starts a persistent loading message that later endpoint calls can update by reusing the same key. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Alias('Message')] + [string]$Content, + [ValidateSet('info', 'success', 'warning', 'error', 'loading')] + [string]$Type = 'info', + [double]$Duration, + [string]$Key, + [switch]$Broadcast + ) + + if (-not $DashboardHub) { + throw 'Show-AntDesignMessage requires the PowerShell Universal DashboardHub context.' + } + + $targetConnectionId = Get-Variable -Name 'ConnectionId' -ValueOnly -ErrorAction Ignore + + $data = @{ + content = $Content + type = $Type + } + + if ($PSBoundParameters.ContainsKey('Duration')) { + $data.duration = $Duration + } + + if ($PSBoundParameters.ContainsKey('Key')) { + $data.key = $Key + } + + if ($Broadcast) { + $DashboardHub.SendWebSocketMessage('antdesign-message', $data) + return + } + + if ([string]::IsNullOrWhiteSpace($targetConnectionId)) { + throw 'Show-AntDesignMessage requires -Broadcast or a ConnectionId in the current dashboard endpoint context.' + } + + $DashboardHub.SendWebSocketMessage($targetConnectionId, 'antdesign-message', $data) +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Rate.ps1 b/Apps/Frameworks/AntDesign/Public/Rate.ps1 new file mode 100644 index 0000000..069268a --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Rate.ps1 @@ -0,0 +1,175 @@ +function New-UDAntDesignRate { + <# + .SYNOPSIS + Creates an Ant Design rate descriptor. + + .DESCRIPTION + Creates an antd-rate descriptor that maps the PowerShell command surface to the Ant Design Rate component used by the client runtime. The command mirrors the core Rate API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks. + + .NOTES + Show evaluation. + A quick rating operation on something. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER AllowClear + Whether clicking the current value clears the selection. + + .PARAMETER AllowHalf + Enables half-step selection. + + .PARAMETER Character + Specifies custom content rendered for each rating character. Provide plain text, descriptor content, or an Ant Design icon name. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design rate element. + + .PARAMETER Count + Specifies how many rating characters are displayed. + + .PARAMETER DefaultValue + Specifies the initial selected value. + + .PARAMETER Disabled + Makes the rate read only. + + .PARAMETER Keyboard + Enables keyboard interactions for the control. + + .PARAMETER Size + Specifies the Ant Design rate size. Valid values are small, medium, and large. + + .PARAMETER Style + Specifies inline styles applied to the Ant Design rate element. + + .PARAMETER Tooltips + Specifies tooltip text for each rating character. + + .PARAMETER Value + Specifies the current selected value. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered rate. Keys are emitted as data-* attributes. + + .PARAMETER OnChange + Specifies the endpoint invoked when the selected value changes. + + .PARAMETER OnHoverChange + Specifies the endpoint invoked when the hovered value changes. + + .EXAMPLE + # Basic + New-UDAntDesignRate + + Demonstrates the default Ant Design rating control. + + .EXAMPLE + # Sizes + @( + New-UDAntDesignRate -Size small -DefaultValue 2 + New-UDAntDesignRate -DefaultValue 3 + New-UDAntDesignRate -Size large -DefaultValue 4 + ) + + Shows the small, medium, and large Ant Design rate sizes. + + .EXAMPLE + # Half star + New-UDAntDesignRate -AllowHalf:$true -DefaultValue 2.5 + + Enables half-step selection like the upstream half star example. + + .EXAMPLE + # Show copywriting + @( + New-UDAntDesignRate -Tooltips @('terrible', 'bad', 'normal', 'good', 'wonderful') + New-UDAntDesignTypography -Text 'normal' + ) + + Adds tooltip copy alongside the rating control to mirror the upstream copywriting example. + + .EXAMPLE + # Read only + New-UDAntDesignRate -Disabled -DefaultValue 3 + + Renders a non-interactive rating control. + + .EXAMPLE + # Clear star + @( + New-UDAntDesignTypography -Text 'allowClear: true' + New-UDAntDesignRate -DefaultValue 3 + New-UDAntDesignTypography -Text 'allowClear: false' + New-UDAntDesignRate -AllowClear:$false -DefaultValue 3 + ) + + Demonstrates clearing the current value when clicking the selected character again. + + .EXAMPLE + # Other character + @( + New-UDAntDesignRate -Character 'HeartFilled' + New-UDAntDesignRate -Character 'A' + New-UDAntDesignRate -Character '好' + ) + + Replaces the default star with icon and text characters. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [bool]$AllowClear, + [bool]$AllowHalf, + [object]$Character, + [string]$ClassName, + [int]$Count, + [double]$DefaultValue, + [switch]$Disabled, + [bool]$Keyboard, + [ValidateSet('small', 'medium', 'large')] + [string]$Size, + [hashtable]$Style, + [string[]]$Tooltips, + [double]$Value, + [hashtable]$DataAttributes, + [Endpoint]$OnChange, + [Endpoint]$OnHoverChange + ) + + foreach ($endpoint in @($OnChange, $OnHoverChange)) { + if ($null -ne $endpoint -and $endpoint.PSObject.Methods.Name -contains 'Register') { + $endpoint.Register($Id, $PSCmdlet) + } + } + + $descriptor = @{ + type = 'antd-rate' + id = $Id + } + + foreach ($property in 'AllowClear', 'AllowHalf', 'Character', 'ClassName', 'Count', 'DefaultValue', 'Keyboard', 'Size', 'Style', 'Tooltips', 'Value') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + if ($PSBoundParameters.ContainsKey('Disabled')) { + $descriptor.disabled = [bool]$Disabled + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + if ($null -ne $OnChange) { + $descriptor.onChange = $OnChange + } + + if ($null -ne $OnHoverChange) { + $descriptor.onHoverChange = $OnHoverChange + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Switch.ps1 b/Apps/Frameworks/AntDesign/Public/Switch.ps1 new file mode 100644 index 0000000..e0ee813 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Switch.ps1 @@ -0,0 +1,177 @@ +function New-UDAntDesignSwitch { + <# + .SYNOPSIS + Creates an Ant Design switch descriptor. + + .DESCRIPTION + Creates an antd-switch descriptor that maps the PowerShell command surface to the Ant Design Switch component used by the client runtime. The command mirrors the core Switch API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks. + + .NOTES + If you need to represent the switching between two states or on-off state. + The difference between Switch and Checkbox is that Switch will trigger a state change directly when you toggle it, while Checkbox is generally used for state marking, which should work in conjunction with submit operation. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Checked + Determines whether the switch is on. + + .PARAMETER DefaultChecked + Specifies the initial unchecked or checked state for the switch. + + .PARAMETER Disabled + Disables the switch. + + .PARAMETER Loading + Shows the Ant Design loading state for the switch. + + .PARAMETER Size + Specifies the Ant Design switch size. Valid values are default and small. + + .PARAMETER CheckedChildren + Specifies the content shown when the switch is checked. Provide plain text, descriptor content, or an Ant Design icon name. + + .PARAMETER UncheckedChildren + Specifies the content shown when the switch is unchecked. Provide plain text, descriptor content, or an Ant Design icon name. + + .PARAMETER ClassName + Specifies a class name applied to the Ant Design switch element. + + .PARAMETER ClassNames + Specifies semantic DOM class mappings passed to the Ant Design switch classNames prop. + + .PARAMETER Styles + Specifies semantic DOM inline style mappings passed to the Ant Design switch styles prop. + + .PARAMETER DataAttributes + Specifies custom data attributes added to the rendered switch. Keys are emitted as data-* attributes. + + .PARAMETER OnChange + Specifies the endpoint invoked when the checked state changes. + + .PARAMETER OnClick + Specifies the endpoint invoked when the switch is clicked. + + .PARAMETER Value + Specifies a value sent back through the click and change event payload. + + .EXAMPLE + # Basic + @( + New-UDAntDesignSwitch + New-UDAntDesignSwitch -DefaultChecked $true + ) + + Demonstrates the most basic usage of Ant Design switch descriptors. + + .EXAMPLE + # Disabled + @( + New-UDAntDesignSwitch -Disabled + New-UDAntDesignSwitch -DefaultChecked $true -Disabled + ) + + Shows the disabled state of the Ant Design switch for both unchecked and checked states. + + .EXAMPLE + # Text & icon + @( + New-UDAntDesignSwitch -CheckedChildren '1' -UncheckedChildren '0' + New-UDAntDesignSwitch -DefaultChecked $true -CheckedChildren 'CheckOutlined' -UncheckedChildren 'CloseOutlined' + ) + + Adds checked and unchecked content so the switch can show text and icon states like the upstream examples. + + .EXAMPLE + # Two sizes + @( + New-UDAntDesignSwitch -DefaultChecked $true + New-UDAntDesignSwitch -DefaultChecked $true -Size small + ) + + Uses the default and small Ant Design switch sizes. + + .EXAMPLE + # Loading + @( + New-UDAntDesignSwitch -Loading + New-UDAntDesignSwitch -DefaultChecked $true -Loading + ) + + Marks a pending state of the switch while preserving unchecked and checked loading examples. + + .EXAMPLE + # Custom semantic dom styling + New-UDAntDesignSwitch -DefaultChecked $true -CheckedChildren 'On' -UncheckedChildren 'Off' -Styles @{ + root = @{ + backgroundColor = '#fa8c16' + } + indicator = @{ + backgroundColor = '#fff7e6' + } + } + + Customizes semantic DOM styles by passing style objects through the Ant Design switch styles prop. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [bool]$Checked, + [bool]$DefaultChecked, + [switch]$Disabled, + [switch]$Loading, + [ValidateSet('default', 'small')] + [string]$Size, + [object]$CheckedChildren, + [object]$UncheckedChildren, + [string]$ClassName, + [hashtable]$ClassNames, + [hashtable]$Styles, + [hashtable]$DataAttributes, + [Endpoint]$OnChange, + [Endpoint]$OnClick, + [object]$Value + ) + + foreach ($endpoint in @($OnChange, $OnClick)) { + if ($null -ne $endpoint -and $endpoint.PSObject.Methods.Name -contains 'Register') { + $endpoint.Register($Id, $PSCmdlet) + } + } + + $descriptor = @{ + type = 'antd-switch' + id = $Id + } + + foreach ($property in 'Checked', 'DefaultChecked', 'Size', 'CheckedChildren', 'UncheckedChildren', 'ClassName', 'ClassNames', 'Styles') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + foreach ($switchProperty in 'Disabled', 'Loading') { + if ($PSBoundParameters.ContainsKey($switchProperty)) { + $descriptor[$switchProperty.Substring(0, 1).ToLowerInvariant() + $switchProperty.Substring(1)] = [bool]$PSBoundParameters[$switchProperty] + } + } + + if ($PSBoundParameters.ContainsKey('DataAttributes')) { + $descriptor.dataAttributes = $DataAttributes + } + + if ($null -ne $OnChange) { + $descriptor.onChange = $OnChange + } + + if ($null -ne $OnClick) { + $descriptor.onClick = $OnClick + } + + if ($PSBoundParameters.ContainsKey('Value')) { + $descriptor.value = $Value + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Text.ps1 b/Apps/Frameworks/AntDesign/Public/Text.ps1 new file mode 100644 index 0000000..7d08515 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Text.ps1 @@ -0,0 +1,15 @@ +function New-UDAntDesignText { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [Parameter(Mandatory)] + [string]$Text + ) + + @{ + type = 'antd-text' + id = $Id + text = $Text + } +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/Public/Typography.ps1 b/Apps/Frameworks/AntDesign/Public/Typography.ps1 new file mode 100644 index 0000000..76556b9 --- /dev/null +++ b/Apps/Frameworks/AntDesign/Public/Typography.ps1 @@ -0,0 +1,414 @@ +function New-UDAntDesignTypography { + <# + .SYNOPSIS + Creates an Ant Design typography descriptor. + + .DESCRIPTION + Creates an antd-typography descriptor that maps the PowerShell command surface to the Ant Design Typography family used by the client runtime. The command wraps Typography.Text, Typography.Title, Typography.Paragraph, and Typography.Link so the documented PowerShell examples can follow the same patterns shown in the upstream Ant Design typography docs. + + .NOTES + Basic text writing, including headings, body text, lists, and more. + When you need to display a title or paragraph contents in Articles/Blogs/Notes. + When you need copyable, editable, or ellipsis text treatments. + + .PARAMETER Id + Specifies the component identifier used by PowerShell Universal for state and event routing. + + .PARAMETER Kind + Specifies which Ant Design typography primitive to render. Valid values are text, title, paragraph, and link. + + .PARAMETER Text + Specifies the plain text content rendered by the typography component. + + .PARAMETER Content + Specifies descriptor content rendered by the typography component when you need more than plain text. + + .PARAMETER TypographyType + Specifies the Ant Design typography tone. Valid values are secondary, success, warning, and danger. + + .PARAMETER Level + Specifies the title level for title typography. Valid values are 1 through 5. + + .PARAMETER Href + Specifies the link target when Kind is link. + + .PARAMETER Target + Specifies the anchor target when Kind is link. + + .PARAMETER Code + Renders the content with Ant Design code styling. + + .PARAMETER Delete + Renders the content with deleted text styling. + + .PARAMETER Disabled + Renders the content with disabled styling. + + .PARAMETER Italic + Renders the content with italic styling. + + .PARAMETER Keyboard + Renders the content with keyboard styling. + + .PARAMETER Mark + Renders the content with highlighted styling. + + .PARAMETER Strong + Renders the content with strong emphasis. + + .PARAMETER Underline + Renders the content with underline styling. + + .PARAMETER Copyable + Enables the Ant Design copyable affordance. + + .PARAMETER CopyText + Specifies the text copied when the copyable action is used. + + .PARAMETER CopyIcon + Specifies custom icon content for the copyable action. Provide two items to mirror the Ant Design [copy, copied] icon pair. + + .PARAMETER CopyTooltips + Specifies the copyable tooltips. Pass `$false` to hide them or an array with two values to mirror the default pair. + + .PARAMETER CopyFormat + Specifies the MIME type used for copied content. + + .PARAMETER Editable + Enables the Ant Design editable affordance. + + .PARAMETER EditText + Specifies the editable text value shown while editing. + + .PARAMETER EditIcon + Specifies custom icon content for the edit action. + + .PARAMETER EditTooltip + Specifies the editable tooltip content. Pass `$false` to hide the tooltip. + + .PARAMETER EditTriggerType + Specifies how editing is triggered. Valid values are icon, text, and both. + + .PARAMETER EditEnterIcon + Specifies the confirmation icon shown while editing. Pass `$false` to remove it. + + .PARAMETER EditMaxLength + Specifies the maximum edit length. + + .PARAMETER Ellipsis + Enables Ant Design ellipsis handling. + + .PARAMETER EllipsisRows + Specifies the maximum number of rows rendered before ellipsis is applied. + + .PARAMETER EllipsisExpandable + Adds the Ant Design expand affordance for ellipsis content. + + .PARAMETER EllipsisSuffix + Specifies the suffix preserved at the end of ellipsis content. + + .PARAMETER EllipsisSymbol + Specifies the custom expand label or content. + + .PARAMETER EllipsisTooltip + Specifies tooltip content shown for ellipsis text. Pass `$false` to hide it. + + .PARAMETER EllipsisDefaultExpanded + Expands the ellipsis content by default. + + .PARAMETER OnClick + Specifies the endpoint invoked when the typography component is clicked. + + .PARAMETER OnCopy + Specifies the endpoint invoked after copyable text is copied. + + .PARAMETER OnChange + Specifies the endpoint invoked when editable text is committed. + + .PARAMETER OnEditStart + Specifies the endpoint invoked when editable mode starts. + + .PARAMETER OnEditEnd + Specifies the endpoint invoked when editable mode ends. + + .PARAMETER OnEditCancel + Specifies the endpoint invoked when editable mode is cancelled. + + .PARAMETER OnExpand + Specifies the endpoint invoked when ellipsis content expands or collapses. + + .PARAMETER Value + Specifies the value sent back through click events. + + .EXAMPLE + # Basic + @( + New-UDAntDesignTypography -Kind title -Level 2 -Text 'Typography' + New-UDAntDesignTypography -Kind paragraph -Text 'Basic text writing, including headings, body text, lists, and more.' + New-UDAntDesignTypography -Kind paragraph -Text 'Typography is the foundation for readable titles, paragraphs, links, and inline semantic emphasis in the Ant Design framework.' + ) + + Displays the document-style introduction shown in the upstream typography examples. + + .EXAMPLE + # Title Component + @( + New-UDAntDesignTypography -Kind title -Level 1 -Text 'h1. Ant Design' + New-UDAntDesignTypography -Kind title -Level 2 -Text 'h2. Ant Design' + New-UDAntDesignTypography -Kind title -Level 3 -Text 'h3. Ant Design' + New-UDAntDesignTypography -Kind title -Level 4 -Text 'h4. Ant Design' + New-UDAntDesignTypography -Kind title -Level 5 -Text 'h5. Ant Design' + ) + + Shows the five title levels exposed by the Ant Design Typography.Title wrapper. + + .EXAMPLE + # Text and Link Component + @( + New-UDAntDesignTypography -Text 'Ant Design (default)' + New-UDAntDesignTypography -Text 'Ant Design (secondary)' -TypographyType secondary + New-UDAntDesignTypography -Text 'Ant Design (success)' -TypographyType success + New-UDAntDesignTypography -Text 'Ant Design (warning)' -TypographyType warning + New-UDAntDesignTypography -Text 'Ant Design (danger)' -TypographyType danger + New-UDAntDesignTypography -Text 'Ant Design (disabled)' -Disabled + New-UDAntDesignTypography -Text 'Ant Design (mark)' -Mark + New-UDAntDesignTypography -Text 'Ant Design (code)' -Code + New-UDAntDesignTypography -Text 'Ant Design (keyboard)' -Keyboard + New-UDAntDesignTypography -Text 'Ant Design (underline)' -Underline + New-UDAntDesignTypography -Text 'Ant Design (delete)' -Delete + New-UDAntDesignTypography -Text 'Ant Design (strong)' -Strong + New-UDAntDesignTypography -Text 'Ant Design (italic)' -Italic + New-UDAntDesignTypography -Kind link -Text 'Ant Design (Link)' -Href 'https://ant.design/' -Target '_blank' + ) + + Mirrors the text-style examples from the Ant Design docs, including semantic emphasis and links. + + .EXAMPLE + # Copyable + @( + New-UDAntDesignTypography -Text 'This is a copyable text.' -Copyable + New-UDAntDesignTypography -Text 'Replace copy text.' -Copyable -CopyText 'Hello, Ant Design!' + New-UDAntDesignTypography -Text 'Hide copy tooltips.' -Copyable -CopyTooltips $false + ) + + Enables the Ant Design copy affordance with default, overridden, and tooltip-free variants. + + .EXAMPLE + # Editable + @( + New-UDAntDesignTypography -Kind paragraph -Text 'This is an editable text.' -Editable + New-UDAntDesignTypography -Kind paragraph -Text 'Click the icon or text to start editing.' -Editable -EditTriggerType both -EditTooltip 'Edit typography' + ) + + Uses the Ant Design editable affordance and keeps the edited value in the client preview. + + .EXAMPLE + # Ellipsis + @( + New-UDAntDesignTypography -Kind paragraph -Text 'Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.' -Ellipsis -EllipsisRows 2 -EllipsisExpandable -EllipsisSymbol 'Expand' + New-UDAntDesignTypography -Kind paragraph -Text 'Ant Design, a design language for background applications, is refined by Ant UED Team.' -Ellipsis -EllipsisRows 1 -EllipsisSuffix '--Ant Design' -EllipsisTooltip 'Ellipsis preview' + ) + + Demonstrates expandable and suffix-preserving ellipsis patterns from the upstream docs. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [string]$Id = ([guid]::NewGuid().ToString()), + [ValidateSet('text', 'title', 'paragraph', 'link')] + [string]$Kind = 'text', + [string]$Text, + [object]$Content, + [ValidateSet('secondary', 'success', 'warning', 'danger')] + [string]$TypographyType, + [ValidateRange(1, 5)] + [int]$Level, + [string]$Href, + [ValidateSet('_blank', '_self', '_parent', '_top')] + [string]$Target, + [switch]$Code, + [switch]$Delete, + [switch]$Disabled, + [switch]$Italic, + [switch]$Keyboard, + [switch]$Mark, + [switch]$Strong, + [switch]$Underline, + [switch]$Copyable, + [string]$CopyText, + [object[]]$CopyIcon, + [object]$CopyTooltips, + [ValidateSet('text/plain', 'text/html')] + [string]$CopyFormat, + [switch]$Editable, + [string]$EditText, + [object]$EditIcon, + [object]$EditTooltip, + [ValidateSet('icon', 'text', 'both')] + [string[]]$EditTriggerType, + [object]$EditEnterIcon, + [int]$EditMaxLength, + [switch]$Ellipsis, + [int]$EllipsisRows, + [switch]$EllipsisExpandable, + [string]$EllipsisSuffix, + [object]$EllipsisSymbol, + [object]$EllipsisTooltip, + [bool]$EllipsisDefaultExpanded, + [Endpoint]$OnClick, + [Endpoint]$OnCopy, + [Endpoint]$OnChange, + [Endpoint]$OnEditStart, + [Endpoint]$OnEditEnd, + [Endpoint]$OnEditCancel, + [Endpoint]$OnExpand, + [object]$Value + ) + + if (-not $PSBoundParameters.ContainsKey('Text') -and -not $PSBoundParameters.ContainsKey('Content')) { + throw 'New-UDAntDesignTypography requires -Text or -Content.' + } + + foreach ($endpoint in @($OnClick, $OnCopy, $OnChange, $OnEditStart, $OnEditEnd, $OnEditCancel, $OnExpand)) { + if ($null -ne $endpoint -and $endpoint.PSObject.Methods.Name -contains 'Register') { + $endpoint.Register($Id, $PSCmdlet) + } + } + + $descriptor = @{ + type = 'antd-typography' + id = $Id + kind = $Kind + } + + if ($PSBoundParameters.ContainsKey('Text')) { + $descriptor.text = $Text + } + + if ($PSBoundParameters.ContainsKey('Content')) { + $descriptor.content = $Content + } + + foreach ($property in 'TypographyType', 'Level', 'Href', 'Target') { + if ($PSBoundParameters.ContainsKey($property)) { + $descriptor[$property.Substring(0, 1).ToLowerInvariant() + $property.Substring(1)] = $PSBoundParameters[$property] + } + } + + foreach ($switchProperty in 'Code', 'Delete', 'Disabled', 'Italic', 'Keyboard', 'Mark', 'Strong', 'Underline') { + if ($PSBoundParameters.ContainsKey($switchProperty)) { + $descriptor[$switchProperty.Substring(0, 1).ToLowerInvariant() + $switchProperty.Substring(1)] = [bool]$PSBoundParameters[$switchProperty] + } + } + + if ($PSBoundParameters.ContainsKey('Copyable') -or $PSBoundParameters.ContainsKey('CopyText') -or $PSBoundParameters.ContainsKey('CopyIcon') -or $PSBoundParameters.ContainsKey('CopyTooltips') -or $PSBoundParameters.ContainsKey('CopyFormat')) { + $copyableDescriptor = @{} + + if ($PSBoundParameters.ContainsKey('CopyText')) { + $copyableDescriptor['text'] = $CopyText + } + + if ($PSBoundParameters.ContainsKey('CopyIcon')) { + $copyableDescriptor['icon'] = @($CopyIcon) + } + + if ($PSBoundParameters.ContainsKey('CopyTooltips')) { + $copyableDescriptor['tooltips'] = $CopyTooltips + } + + if ($PSBoundParameters.ContainsKey('CopyFormat')) { + $copyableDescriptor['format'] = $CopyFormat + } + + if ($copyableDescriptor.Count -eq 0 -and $Copyable) { + $descriptor.copyable = $true + } + else { + $descriptor.copyable = $copyableDescriptor + } + } + + if ($PSBoundParameters.ContainsKey('Editable') -or $PSBoundParameters.ContainsKey('EditText') -or $PSBoundParameters.ContainsKey('EditIcon') -or $PSBoundParameters.ContainsKey('EditTooltip') -or $PSBoundParameters.ContainsKey('EditTriggerType') -or $PSBoundParameters.ContainsKey('EditEnterIcon') -or $PSBoundParameters.ContainsKey('EditMaxLength')) { + $editableDescriptor = @{} + + if ($PSBoundParameters.ContainsKey('EditText')) { + $editableDescriptor['text'] = $EditText + } + + if ($PSBoundParameters.ContainsKey('EditIcon')) { + $editableDescriptor['icon'] = $EditIcon + } + + if ($PSBoundParameters.ContainsKey('EditTooltip')) { + $editableDescriptor['tooltip'] = $EditTooltip + } + + if ($PSBoundParameters.ContainsKey('EditTriggerType')) { + $editableDescriptor['triggerType'] = @($EditTriggerType) + } + + if ($PSBoundParameters.ContainsKey('EditEnterIcon')) { + $editableDescriptor['enterIcon'] = $EditEnterIcon + } + + if ($PSBoundParameters.ContainsKey('EditMaxLength')) { + $editableDescriptor['maxLength'] = $EditMaxLength + } + + if ($editableDescriptor.Count -eq 0 -and $Editable) { + $descriptor.editable = $true + } + else { + $descriptor.editable = $editableDescriptor + } + } + + if ($PSBoundParameters.ContainsKey('Ellipsis') -or $PSBoundParameters.ContainsKey('EllipsisRows') -or $PSBoundParameters.ContainsKey('EllipsisExpandable') -or $PSBoundParameters.ContainsKey('EllipsisSuffix') -or $PSBoundParameters.ContainsKey('EllipsisSymbol') -or $PSBoundParameters.ContainsKey('EllipsisTooltip') -or $PSBoundParameters.ContainsKey('EllipsisDefaultExpanded')) { + $ellipsisDescriptor = @{} + + if ($PSBoundParameters.ContainsKey('EllipsisRows')) { + $ellipsisDescriptor['rows'] = $EllipsisRows + } + + if ($PSBoundParameters.ContainsKey('EllipsisExpandable')) { + $ellipsisDescriptor['expandable'] = [bool]$EllipsisExpandable + } + + if ($PSBoundParameters.ContainsKey('EllipsisSuffix')) { + $ellipsisDescriptor['suffix'] = $EllipsisSuffix + } + + if ($PSBoundParameters.ContainsKey('EllipsisSymbol')) { + $ellipsisDescriptor['symbol'] = $EllipsisSymbol + } + + if ($PSBoundParameters.ContainsKey('EllipsisTooltip')) { + $ellipsisDescriptor['tooltip'] = $EllipsisTooltip + } + + if ($PSBoundParameters.ContainsKey('EllipsisDefaultExpanded')) { + $ellipsisDescriptor['defaultExpanded'] = $EllipsisDefaultExpanded + } + + if ($ellipsisDescriptor.Count -eq 0 -and $Ellipsis) { + $descriptor.ellipsis = $true + } + else { + $descriptor.ellipsis = $ellipsisDescriptor + } + } + + foreach ($endpointProperty in 'OnClick', 'OnCopy', 'OnChange', 'OnEditStart', 'OnEditEnd', 'OnEditCancel', 'OnExpand') { + $endpointValue = Get-Variable -Name $endpointProperty -ValueOnly + + if ($null -ne $endpointValue) { + $descriptor[$endpointProperty.Substring(0, 1).ToLowerInvariant() + $endpointProperty.Substring(1)] = $endpointValue + } + } + + if ($PSBoundParameters.ContainsKey('Value')) { + $descriptor.value = $Value + } + + $descriptor +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/README.md b/Apps/Frameworks/AntDesign/README.md new file mode 100644 index 0000000..82c9816 --- /dev/null +++ b/Apps/Frameworks/AntDesign/README.md @@ -0,0 +1,116 @@ +# Ant Design Dashboard Framework + +This folder scaffolds an alternate PowerShell Universal dashboard framework that keeps the PSU transport and descriptor contract while rendering with Ant Design. + +## Design choices + +- React, TypeScript, Vite, SignalR, Zustand, Zod, and TanStack Query provide the client runtime foundation. +- `withComponentFeatures` is imported from the published `universal-dashboard` npm package instead of being reimplemented locally. +- Static assets are built into `dist` and exposed through `.universal/publishedFolders.ps1` at `/frameworks/ant-design`. + +## Build + +```powershell +npm install +npm run build +``` + +## Local harness workflow + +Framework iteration should run against the harness first. + +From this folder: + +```powershell +npm install +npm run harness +``` + +That builds the Ant Design bundle and starts the harness from [Apps/Frameworks/Harness](d:/git/powershell-universal-gallery/Apps/Frameworks/Harness) on `http://127.0.0.1:5057`. + +The AntDesign harness definition mounts this framework bundle and uses `New-AntDesignDemo` by default, so local work can stay focused on the framework transport contract without starting full PSU. + +From [Apps/Frameworks](d:/git/powershell-universal-gallery/Apps/Frameworks), you can also use the shared launcher: + +```powershell +.\Start-Framework.ps1 .\AntDesign +.\Start-Framework.ps1 .\AntDesign -Build +``` + +The `-Build` switch runs the framework build before starting the harness. + +## Module helpers + +Importing the PowerShell module exposes: + +- `Get-PSUAntDesignFrameworkAssetBasePath` +- `Get-PSUAntDesignFrameworkEntryPoint` +- `New-UDAntDesignText` +- `New-UDAntDesignButton` +- `New-UDAntDesignCheckbox` +- `New-UDAntDesignCol` +- `New-UDAntDesignInput` +- `New-UDAntDesignLayout` +- `New-UDAntDesignLayoutContent` +- `New-UDAntDesignLayoutFooter` +- `New-UDAntDesignLayoutHeader` +- `New-UDAntDesignLayoutSider` +- `New-UDAntDesignRate` +- `New-UDAntDesignRow` +- `New-UDAntDesignSwitch` +- `New-UDAntDesignTypography` +- `Show-AntDesignMessage` +- `New-AntDesignDemo` +- `New-AntDesignDemoApp` + +The second helper resolves the current build manifest and returns the hashed asset paths for the compiled entrypoint. +`New-AntDesignDemo` returns a component documentation shell for the framework, with per-component pages and live examples generated from the module comment-based help. `New-AntDesignDemoApp` wraps that content in `New-UDApp` for PSU-hosted use. +The `Endpoint` type used by interactive component helpers is supplied by PowerShell Universal or by the local harness runner. + +## Scope of the scaffold + +The current scaffold includes: + +- strict TypeScript and Vite build configuration +- hashed build assets with manifest-driven entrypoint resolution for cache busting +- a published-folder module layout for PSU +- descriptor schemas and runtime state store +- dashboard bootstrap over `/api/internal/dashboard` +- SignalR connection scaffolding for `/dashboardhub` +- server-push support for Ant Design global messages via `Show-AntDesignMessage` +- a global component registry with lazy-loaded Ant Design components wrapped by `withComponentFeatures` +- a help-driven component documentation shell with live previews for the documented examples + +## Demo usage + +Load the module from PowerShell Universal or through the harness runner, then call: + +```powershell +New-AntDesignDemo +``` + +The default demo now opens an Ant Design-style docs experience. The button, checkbox, grid, input, layout, rate, switch, and typography pages are generated from comment-based help, and the preview cards are rendered by executing those documented examples. + +Inside PowerShell Universal, you can use: + +```powershell +New-AntDesignDemoApp +``` + +For harness-hosted iteration, open `http://127.0.0.1:5057` after `npm run harness`. + +## Playwright + +Browser-level tests run against the harness by default. + +```powershell +npm run test:e2e:install +npm run test:e2e +``` + +The shared fixture in [Apps/Frameworks/AntDesign/testing/playwright/harnessFixture.ts](d:/git/powershell-universal-gallery/Apps/Frameworks/AntDesign/testing/playwright/harnessFixture.ts) drives server-push scenarios through the harness admin endpoints: + +- `POST /api/harness/messages` +- `POST /api/harness/downloads/{id}` + +The runtime now handles server-pushed Ant Design global messages in addition to bootstrap and connection status. diff --git a/Apps/Frameworks/AntDesign/dashboard.ps1 b/Apps/Frameworks/AntDesign/dashboard.ps1 new file mode 100644 index 0000000..51e8f7e --- /dev/null +++ b/Apps/Frameworks/AntDesign/dashboard.ps1 @@ -0,0 +1,15 @@ +$moduleRoot = if (-not [string]::IsNullOrWhiteSpace($HarnessScriptRoot)) { + $HarnessScriptRoot +} +else { + $PSScriptRoot +} + +$modulePath = Join-Path $moduleRoot 'Devolutions.PowerShellUniversal.Frameworks.AntDesign.psd1' +Import-Module $modulePath -Force + +@{ + dashboard = New-AntDesignDemo + dashboardName = 'Ant Design Demo' + developerLicense = $true +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/eslint.config.js b/Apps/Frameworks/AntDesign/eslint.config.js new file mode 100644 index 0000000..16aed24 --- /dev/null +++ b/Apps/Frameworks/AntDesign/eslint.config.js @@ -0,0 +1,27 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist'], + }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['src/**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +); diff --git a/Apps/Frameworks/AntDesign/harness.ps1 b/Apps/Frameworks/AntDesign/harness.ps1 new file mode 100644 index 0000000..05df907 --- /dev/null +++ b/Apps/Frameworks/AntDesign/harness.ps1 @@ -0,0 +1,55 @@ +function Get-AntDesignHarnessEntryPoint { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$DistPath, + + [Parameter(Mandatory)] + [string]$BasePath + ) + + $manifestPath = Join-Path -Path $DistPath -ChildPath 'manifest.json' + + if (-not (Test-Path -Path $manifestPath)) { + throw "Ant Design framework manifest not found at '$manifestPath'. Run npm run build in Apps/Frameworks/AntDesign first." + } + + $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json -AsHashtable + $entry = $manifest['index.html'] + + if (-not $entry) { + throw "Ant Design framework manifest entry for 'index.html' was not found in '$manifestPath'." + } + + $styles = @() + + if ($entry.ContainsKey('css') -and $entry.css.Count -gt 0) { + $styles = @("$BasePath/$($entry.css[0] -replace '\\', '/')") + } + + @{ + Scripts = @("$BasePath/$($entry.file -replace '\\', '/')") + Styles = $styles + } +} + +$antDesignDistPath = Join-Path $HarnessScriptRoot 'dist' +$antDesignAssetBasePath = '/frameworks/ant-design' +$antDesignEntryPoint = Get-AntDesignHarnessEntryPoint -DistPath $antDesignDistPath -BasePath $antDesignAssetBasePath + +@{ + DashboardScript = Join-Path $HarnessScriptRoot 'dashboard.ps1' + EndpointRoot = Join-Path $HarnessScriptRoot '..\Harness\sample\endpoints' + StaticAssets = @( + @{ + RequestPath = $antDesignAssetBasePath + Path = $antDesignDistPath + } + ) + Shell = @{ + Title = 'PSU Framework Harness - Ant Design' + MountId = 'root' + Scripts = $antDesignEntryPoint.Scripts + Styles = $antDesignEntryPoint.Styles + } +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/index.html b/Apps/Frameworks/AntDesign/index.html new file mode 100644 index 0000000..b7c1430 --- /dev/null +++ b/Apps/Frameworks/AntDesign/index.html @@ -0,0 +1,14 @@ + + + + + + + + PSU Ant Design Framework + + +
+ + + diff --git a/Apps/Frameworks/AntDesign/package-lock.json b/Apps/Frameworks/AntDesign/package-lock.json new file mode 100644 index 0000000..f9f76bb --- /dev/null +++ b/Apps/Frameworks/AntDesign/package-lock.json @@ -0,0 +1,4373 @@ +{ + "name": "devolutions-powershelluniversal-frameworks-antdesign", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devolutions-powershelluniversal-frameworks-antdesign", + "version": "0.1.0", + "dependencies": { + "@ant-design/icons": "^5.6.1", + "@microsoft/signalr": "^8.0.7", + "@tanstack/react-query": "^5.59.0", + "antd": "^5.21.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^5.0.0", + "universal-dashboard": "^1.1.2", + "zod": "^3.23.8", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@playwright/test": "^1.54.1", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.7.1", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.12.0", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0", + "vite": "^6.0.1" + } + }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@microsoft/signalr": { + "version": "8.0.17", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.17.tgz", + "integrity": "sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.1.tgz", + "integrity": "sha512-T03+Wk31Kz/28OC+rLlHtSNwD5Io3OWw6rPFPAp898sqALB/XnTrr3trB3mPoj379v0aRaW6t09HUG6dUyHR3g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.2.tgz", + "integrity": "sha512-CTXG18eP3sO3gc+96ep9HyVI/RzMup7L59apM/D0wWo1SHRdwOb7xyD4bMbmpu4dPlTch59Kxb8lU7U9ME60fg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz", + "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.40", + "@swc/core-darwin-x64": "1.15.40", + "@swc/core-linux-arm-gnueabihf": "1.15.40", + "@swc/core-linux-arm64-gnu": "1.15.40", + "@swc/core-linux-arm64-musl": "1.15.40", + "@swc/core-linux-ppc64-gnu": "1.15.40", + "@swc/core-linux-s390x-gnu": "1.15.40", + "@swc/core-linux-x64-gnu": "1.15.40", + "@swc/core-linux-x64-musl": "1.15.40", + "@swc/core-win32-arm64-msvc": "1.15.40", + "@swc/core-win32-ia32-msvc": "1.15.40", + "@swc/core-win32-x64-msvc": "1.15.40" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", + "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz", + "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz", + "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz", + "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz", + "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz", + "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz", + "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz", + "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz", + "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz", + "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz", + "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz", + "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.1.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.1", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.11.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-error-boundary": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz", + "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-dashboard": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/universal-dashboard/-/universal-dashboard-1.1.2.tgz", + "integrity": "sha512-l3izS6mklhi0mRbF4w8a2z2JBhk8yKYbuQcWrintqyb5qzxK3s8uF9zidSTmSQWiurVmvQkMOa0HY0/5MnIb7g==", + "license": "ISC" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/Apps/Frameworks/AntDesign/package.json b/Apps/Frameworks/AntDesign/package.json new file mode 100644 index 0000000..3c35c0d --- /dev/null +++ b/Apps/Frameworks/AntDesign/package.json @@ -0,0 +1,43 @@ +{ + "name": "devolutions-powershelluniversal-frameworks-antdesign", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "harness": "node ./scripts/runHarness.mjs", + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed", + "test:e2e:install": "playwright install chromium" + }, + "dependencies": { + "@ant-design/icons": "^5.6.1", + "@microsoft/signalr": "^8.0.7", + "@tanstack/react-query": "^5.59.0", + "antd": "^5.21.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^5.0.0", + "universal-dashboard": "^1.1.2", + "zod": "^3.23.8", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@playwright/test": "^1.54.1", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.7.1", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.12.0", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0", + "vite": "^6.0.1" + } +} diff --git a/Apps/Frameworks/AntDesign/playwright.config.ts b/Apps/Frameworks/AntDesign/playwright.config.ts new file mode 100644 index 0000000..d131e1c --- /dev/null +++ b/Apps/Frameworks/AntDesign/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'node:path'; + +const harnessUrl = process.env.PSU_HARNESS_URL ?? 'http://127.0.0.1:5057'; +const harnessDefinitionPath = path.join(process.cwd(), 'harness.ps1'); + +export default defineConfig({ + testDir: './testing/playwright', + fullyParallel: true, + reporter: 'list', + use: { + baseURL: harnessUrl, + trace: 'on-first-retry', + }, + webServer: { + command: 'npm run harness', + cwd: process.cwd(), + env: { + ...process.env, + Harness__DefinitionPath: harnessDefinitionPath, + }, + url: harnessUrl, + reuseExistingServer: true, + timeout: 180000, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/scripts/runHarness.mjs b/Apps/Frameworks/AntDesign/scripts/runHarness.mjs new file mode 100644 index 0000000..f89bbad --- /dev/null +++ b/Apps/Frameworks/AntDesign/scripts/runHarness.mjs @@ -0,0 +1,50 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import process from 'node:process'; + +const cwd = process.cwd(); +const harnessDefinitionPath = path.join(cwd, 'harness.ps1'); +const harnessProject = path.join( + cwd, + '..', + 'Harness', + 'src', + 'PowerShellUniversal.Frameworks.Harness', + 'PowerShellUniversal.Frameworks.Harness.csproj', +); +const harnessUrl = process.env.PSU_HARNESS_URL ?? 'http://127.0.0.1:5057'; +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const dotnetCommand = process.platform === 'win32' ? 'dotnet.exe' : 'dotnet'; + +function run(command, args, extraEnv = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + env: { + ...process.env, + ...extraEnv, + }, + shell: process.platform === 'win32', + stdio: 'inherit', + }); + + child.on('error', reject); + child.on('exit', (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(`${command} exited with code ${code ?? 'unknown'}.`)); + }); + }); +} + +await run(npmCommand, ['run', 'build']); +await run( + dotnetCommand, + ['run', '--project', harnessProject, '--urls', harnessUrl], + { + Harness__DefinitionPath: harnessDefinitionPath, + }, +); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/app/App.tsx b/Apps/Frameworks/AntDesign/src/app/App.tsx new file mode 100644 index 0000000..c42d775 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/app/App.tsx @@ -0,0 +1,182 @@ +import { Alert, App as AntdApp, ConfigProvider, Layout, Segmented, Spin, Typography, theme } from 'antd'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useEffect, useMemo, useState } from 'react'; +import { + applyResolvedColorMode, + getColorModeMediaQuery, + persistColorModePreference, + readStoredColorModePreference, + readSystemColorMode, + resolveColorMode, + type ColorModePreference, + type ResolvedColorMode, +} from '../config/colorMode'; +import { useBootstrap } from '../features/bootstrap/useBootstrap'; +import { useDashboardConnection } from '../features/transport/useDashboardConnection'; +import { renderDescriptorNode } from '../registry/renderDescriptor'; +import { useRuntimeStore } from '../state/runtimeStore'; + +type BootstrapStatusProps = { + colorModePreference: ColorModePreference; + onColorModePreferenceChange: (value: ColorModePreference) => void; + resolvedColorMode: ResolvedColorMode; +}; + +function useColorModePreference() { + const [colorModePreference, setColorModePreference] = useState(() => + readStoredColorModePreference(), + ); + const [systemColorMode, setSystemColorMode] = useState(() => readSystemColorMode()); + + useEffect(() => { + const mediaQueryList = window.matchMedia(getColorModeMediaQuery()); + const legacyMediaQueryList = mediaQueryList as MediaQueryList & { + addListener?: (listener: () => void) => void; + removeListener?: (listener: () => void) => void; + }; + const updateSystemColorMode = () => { + setSystemColorMode(mediaQueryList.matches ? 'dark' : 'light'); + }; + + updateSystemColorMode(); + + if ('addEventListener' in mediaQueryList) { + mediaQueryList.addEventListener('change', updateSystemColorMode); + return () => mediaQueryList.removeEventListener('change', updateSystemColorMode); + } + + legacyMediaQueryList.addListener?.(updateSystemColorMode); + return () => legacyMediaQueryList.removeListener?.(updateSystemColorMode); + }, []); + + const resolvedColorMode = resolveColorMode(colorModePreference, systemColorMode); + + useEffect(() => { + persistColorModePreference(colorModePreference); + }, [colorModePreference]); + + useEffect(() => { + applyResolvedColorMode(resolvedColorMode); + }, [resolvedColorMode]); + + return { + colorModePreference, + resolvedColorMode, + setColorModePreference, + }; +} + +function BootstrapStatus({ + colorModePreference, + onColorModePreferenceChange, + resolvedColorMode, +}: BootstrapStatusProps) { + const bootstrapQuery = useBootstrap(); + const setBootstrap = useRuntimeStore((state) => state.setBootstrap); + const descriptorTree = useRuntimeStore((state) => state.descriptorTree); + const connectionStatus = useRuntimeStore((state) => state.connectionStatus); + const transportError = useRuntimeStore((state) => state.transportError); + + useEffect(() => { + if (bootstrapQuery.data) { + setBootstrap(bootstrapQuery.data); + } + }, [bootstrapQuery.data, setBootstrap]); + + useDashboardConnection(); + + if (bootstrapQuery.isLoading) { + return ( +
+ + Bootstrapping dashboard contract... +
+ ); + } + + if (bootstrapQuery.error) { + return ( + + ); + } + + if (!descriptorTree) { + return ( + + ); + } + + return ( + + +
+ PSU Ant Design Framework + + Connection state: {connectionStatus} + +
+
+
+ Theme + onColorModePreferenceChange(value as ColorModePreference)} + /> + + {colorModePreference === 'system' + ? `Following system (${resolvedColorMode})` + : `Pinned to ${resolvedColorMode} mode`} + +
+ {transportError ? : null} +
+
+ {renderDescriptorNode(descriptorTree)} +
+ ); +} + +function ErrorFallback({ error }: { error: Error }) { + return ; +} + +export function App() { + const { colorModePreference, resolvedColorMode, setColorModePreference } = useColorModePreference(); + const themeConfig = useMemo( + () => ({ + algorithm: resolvedColorMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm, + cssVar: true, + }), + [resolvedColorMode], + ); + + return ( + + + + + + + + ); +} diff --git a/Apps/Frameworks/AntDesign/src/app/app.css b/Apps/Frameworks/AntDesign/src/app/app.css new file mode 100644 index 0000000..d509831 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/app/app.css @@ -0,0 +1,228 @@ +:root { + color-scheme: light; + color: #111827; + --shell-text: #111827; + --shell-page-bg: #f5f5f5; + --shell-panel-bg: rgba(255, 255, 255, 0.7); + --shell-panel-strong-bg: rgba(255, 255, 255, 0.82); + --shell-border: rgba(17, 24, 39, 0.08); + --shell-shadow: 0 22px 50px rgba(15, 23, 42, 0.08); + --shell-preview-bg: rgba(255, 255, 255, 0.96); + --shell-hero-bg: rgba(255, 255, 255, 0.96); + background: var(--shell-page-bg); + font-family: 'Segoe UI', sans-serif; +} + +:root[data-color-mode='dark'] { + color-scheme: dark; + color: #e5edf8; + --shell-text: #e5edf8; + --shell-page-bg: #141414; + --shell-panel-bg: rgba(9, 18, 32, 0.78); + --shell-panel-strong-bg: rgba(13, 23, 39, 0.88); + --shell-border: rgba(148, 163, 184, 0.18); + --shell-shadow: 0 22px 50px rgba(2, 6, 23, 0.45); + --shell-preview-bg: rgba(9, 18, 32, 0.96); + --shell-hero-bg: rgba(11, 18, 31, 0.96); +} + +body { + margin: 0; + min-height: 100vh; + color: var(--shell-text); + background: transparent; +} + +#root { + min-height: 100vh; +} + +.shell-layout { + min-height: 100vh; + background: transparent; +} + +.shell-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + min-height: 120px; + padding: 32px; + color: var(--shell-text); + background: var(--shell-panel-bg); + backdrop-filter: blur(14px); + border-bottom: 1px solid var(--shell-border); +} + +.shell-header .ant-typography, +.shell-header .ant-typography strong { + color: inherit; +} + +.shell-header-copy { + min-width: 0; +} + +.shell-header-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; + width: min(100%, 360px); +} + +.shell-header-actions .ant-alert { + width: 100%; +} + +.shell-theme-control { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + width: 100%; +} + +.shell-theme-caption { + text-transform: capitalize; +} + +.shell-content { + padding: 32px; +} + +.shell-state { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 16px; +} + +.component-card { + border-radius: 24px; + box-shadow: var(--shell-shadow); +} + +.docs-shell { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 24px; + align-items: start; +} + +.docs-sidebar { + position: sticky; + top: 24px; +} + +.docs-sidebar-card, +.docs-section-card, +.docs-overview-card, +.docs-hero { + border-radius: 24px; + box-shadow: var(--shell-shadow); +} + +.docs-sidebar-card { + margin-bottom: 16px; + background: var(--shell-panel-strong-bg); +} + +.docs-menu { + border-radius: 24px; + padding: 12px; + background: var(--shell-panel-strong-bg); + box-shadow: var(--shell-shadow); +} + +.docs-main, +.docs-body, +.docs-example-list { + width: 100%; +} + +.docs-body { + display: flex; + flex-direction: column; + gap: 24px; +} + +.docs-hero { + background: var(--shell-hero-bg); +} + +.docs-kicker { + width: fit-content; +} + +.docs-overview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; +} + +.docs-preview-surface { + display: flex; + flex-direction: column; + gap: 16px; + padding: 24px; + border: 1px solid var(--shell-border); + border-radius: 20px; + background: var(--shell-preview-bg); +} + +.docs-preview-surface-button { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 12px; +} + +.docs-preview-surface-button > * { + flex: 0 0 auto; +} + +.docs-preview-surface > .ant-switch { + align-self: flex-start; + width: auto; +} + +.docs-code-block { + overflow-x: auto; + margin: 0; + padding: 20px; + border-radius: 20px; + color: #e2e8f0; + background: #0f172a; +} + +.docs-code-block code { + font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace; +} + +.docs-parameter-table .ant-table { + border-radius: 18px; +} + +@media (max-width: 960px) { + .shell-header { + flex-direction: column; + align-items: stretch; + } + + .shell-header-actions { + align-items: stretch; + width: 100%; + } + + .docs-shell { + grid-template-columns: 1fr; + } + + .docs-sidebar { + position: static; + } +} diff --git a/Apps/Frameworks/AntDesign/src/components/AntdButton.tsx b/Apps/Frameworks/AntDesign/src/components/AntdButton.tsx new file mode 100644 index 0000000..0e081f6 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdButton.tsx @@ -0,0 +1,132 @@ +import * as AntdIcons from '@ant-design/icons'; +import { Button } from 'antd'; +import type { ButtonProps as AntdButtonComponentProps } from 'antd'; +import { createElement, type ComponentType, type ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdButtonProps = { + autoInsertSpace?: boolean; + block?: boolean; + buttonType?: AntdButtonComponentProps['type']; + className?: string; + content?: DescriptorContent; + color?: AntdButtonComponentProps['color']; + danger?: boolean; + dataAttributes?: Record; + disabled?: boolean; + ghost?: boolean; + href?: string; + htmlType?: AntdButtonComponentProps['htmlType']; + id?: string; + icon?: DescriptorContent; + iconPosition?: AntdButtonComponentProps['iconPosition']; + loading?: AntdButtonComponentProps['loading']; + onClick?: (data: { value: unknown }) => void; + render?: (component: DescriptorContent) => ReactNode; + rootClassName?: string; + shape?: AntdButtonComponentProps['shape']; + size?: AntdButtonComponentProps['size']; + text?: string; + variant?: AntdButtonComponentProps['variant']; + value?: unknown; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +function resolveIcon(icon: DescriptorContent | undefined, render?: (component: DescriptorContent) => ReactNode) { + if (typeof icon === 'undefined') { + return undefined; + } + + if (typeof icon === 'string') { + const iconComponent = AntdIcons[icon as keyof typeof AntdIcons]; + + if (typeof iconComponent === 'function') { + return createElement(iconComponent as ComponentType); + } + } + + return renderDescriptorContent(icon, render); +} + +function resolveLoading( + loading: AntdButtonComponentProps['loading'] | undefined, + render?: (component: DescriptorContent) => ReactNode, +) { + if (!loading || typeof loading !== 'object' || !('icon' in loading)) { + return loading; + } + + return { + ...loading, + icon: resolveIcon(loading.icon as DescriptorContent | undefined, render), + }; +} + +export function AntdButton({ + autoInsertSpace, + block, + buttonType, + className, + content, + color, + danger, + dataAttributes, + disabled, + ghost, + href, + htmlType, + id, + icon, + iconPosition, + loading, + onClick, + render, + rootClassName, + shape, + size, + text, + value, + variant, +}: AntdButtonProps) { + const buttonContent = typeof text !== 'undefined' ? text : renderDescriptorContent(content, render); + const resolvedIcon = resolveIcon(icon, render); + const resolvedLoading = resolveLoading(loading, render); + const buttonProps: AntdButtonComponentProps & { id?: string } = { + ...(typeof autoInsertSpace === 'undefined' ? {} : { autoInsertSpace }), + ...(typeof block === 'undefined' ? {} : { block }), + ...(typeof buttonType === 'undefined' ? {} : { type: buttonType }), + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof color === 'undefined' ? {} : { color }), + ...(typeof danger === 'undefined' ? {} : { danger }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof ghost === 'undefined' ? {} : { ghost }), + ...(typeof href === 'undefined' ? {} : { href }), + ...(typeof htmlType === 'undefined' ? {} : { htmlType }), + ...(typeof iconPosition === 'undefined' ? {} : { iconPosition }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof resolvedLoading === 'undefined' ? {} : { loading: resolvedLoading }), + ...(typeof resolvedIcon === 'undefined' ? {} : { icon: resolvedIcon }), + ...(typeof rootClassName === 'undefined' ? {} : { rootClassName }), + ...(typeof shape === 'undefined' ? {} : { shape }), + ...(typeof size === 'undefined' ? {} : { size }), + ...(typeof variant === 'undefined' ? {} : { variant }), + ...(typeof onClick === 'undefined' ? {} : { onClick: () => onClick({ value }) }), + ...toDataAttributeProps(dataAttributes), + }; + + return ( + + ); +} diff --git a/Apps/Frameworks/AntDesign/src/components/AntdCheckbox.tsx b/Apps/Frameworks/AntDesign/src/components/AntdCheckbox.tsx new file mode 100644 index 0000000..5758914 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdCheckbox.tsx @@ -0,0 +1,92 @@ +import { Checkbox as AntdCheckboxControl } from 'antd'; +import type { CheckboxChangeEvent, CheckboxProps as AntdCheckboxComponentProps } from 'antd'; +import { useEffect, useState, type CSSProperties, type ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdCheckboxProps = { + autoFocus?: boolean; + checked?: boolean; + className?: string; + classNames?: Record; + dataAttributes?: Record; + defaultChecked?: boolean; + disabled?: boolean; + id?: string; + indeterminate?: boolean; + label?: DescriptorContent; + onChange?: (data: { checked: boolean; value: unknown }) => void; + render?: (component: DescriptorContent) => ReactNode; + style?: CSSProperties; + styles?: Record; + value?: unknown; +}; + +type AntdCheckboxRuntimeProps = AntdCheckboxComponentProps & { + classNames?: Record; + dataAttributes?: Record; + id?: string; + styles?: Record; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +export function AntdCheckbox({ + autoFocus, + checked, + className, + classNames, + dataAttributes, + defaultChecked, + disabled, + id, + indeterminate, + label, + onChange, + render, + style, + styles, + value, +}: AntdCheckboxProps) { + const [currentChecked, setCurrentChecked] = useState(checked ?? defaultChecked ?? false); + + useEffect(() => { + if (typeof checked !== 'undefined') { + setCurrentChecked(checked); + return; + } + + if (typeof defaultChecked !== 'undefined') { + setCurrentChecked(defaultChecked); + } + }, [checked, defaultChecked]); + + const checkboxProps: AntdCheckboxRuntimeProps = { + ...(typeof autoFocus === 'undefined' ? {} : { autoFocus }), + checked: currentChecked, + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof classNames === 'undefined' ? {} : { classNames }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof indeterminate === 'undefined' ? {} : { indeterminate }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof styles === 'undefined' ? {} : { styles }), + ...(typeof value === 'undefined' ? {} : { value }), + onChange: (event: CheckboxChangeEvent) => { + const nextChecked = event.target.checked; + setCurrentChecked(nextChecked); + onChange?.({ checked: nextChecked, value }); + }, + ...toDataAttributeProps(dataAttributes), + }; + + return {renderDescriptorContent(label, render)}; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdCol.tsx b/Apps/Frameworks/AntDesign/src/components/AntdCol.tsx new file mode 100644 index 0000000..b887ab8 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdCol.tsx @@ -0,0 +1,78 @@ +import { Col } from 'antd'; +import type { ColProps as AntdColComponentProps } from 'antd'; +import type { CSSProperties, ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdColProps = { + className?: string; + content?: DescriptorContent; + dataAttributes?: Record; + flex?: AntdColComponentProps['flex']; + id?: string; + lg?: AntdColComponentProps['lg']; + md?: AntdColComponentProps['md']; + offset?: AntdColComponentProps['offset']; + order?: AntdColComponentProps['order']; + pull?: AntdColComponentProps['pull']; + push?: AntdColComponentProps['push']; + render?: (component: DescriptorContent) => ReactNode; + sm?: AntdColComponentProps['sm']; + span?: AntdColComponentProps['span']; + style?: CSSProperties; + xl?: AntdColComponentProps['xl']; + xs?: AntdColComponentProps['xs']; + xxl?: AntdColComponentProps['xxl']; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +export function AntdCol({ + className, + content, + dataAttributes, + flex, + id, + lg, + md, + offset, + order, + pull, + push, + render, + sm, + span, + style, + xl, + xs, + xxl, +}: AntdColProps) { + const colProps: AntdColComponentProps & { id?: string } = { + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof flex === 'undefined' ? {} : { flex }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof lg === 'undefined' ? {} : { lg }), + ...(typeof md === 'undefined' ? {} : { md }), + ...(typeof offset === 'undefined' ? {} : { offset }), + ...(typeof order === 'undefined' ? {} : { order }), + ...(typeof pull === 'undefined' ? {} : { pull }), + ...(typeof push === 'undefined' ? {} : { push }), + ...(typeof sm === 'undefined' ? {} : { sm }), + ...(typeof span === 'undefined' ? {} : { span }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof xl === 'undefined' ? {} : { xl }), + ...(typeof xs === 'undefined' ? {} : { xs }), + ...(typeof xxl === 'undefined' ? {} : { xxl }), + ...toDataAttributeProps(dataAttributes), + }; + + return {renderDescriptorContent(content, render)}; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdDocs.tsx b/Apps/Frameworks/AntDesign/src/components/AntdDocs.tsx new file mode 100644 index 0000000..2eaa1fa --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdDocs.tsx @@ -0,0 +1,293 @@ +import { BookOutlined, LinkOutlined } from '@ant-design/icons'; +import { Button, Card, Empty, Menu, Space, Table, Tag, Typography } from 'antd'; +import type { MenuProps, TableColumnsType } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import { renderDescriptorNode } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdDocsParameter = { + description?: string; + name: string; + required?: boolean; + type?: string; + validValues?: string | string[]; +}; + +type AntdDocsExample = { + code: string; + description?: string; + preview?: DescriptorContent; + title: string; +}; + +type AntdDocsComponent = { + category?: string; + commandName?: string; + description?: string; + examples?: AntdDocsExample[]; + key: string; + parameters?: AntdDocsParameter[]; + sourceUrl?: string; + summary?: string; + title: string; + whenToUse?: string[]; +}; + +type AntdDocsProps = { + components?: AntdDocsComponent[]; + overview?: string; + title?: string; +}; + +const overviewRoute = '/overview'; + +function getComponentRoute(componentKey: string) { + return `/components/${componentKey}`; +} + +function normalizeRoute(hash: string, components: AntdDocsComponent[]): string { + const normalizedHash = hash.replace(/^#/, '') || overviewRoute; + + if (normalizedHash === overviewRoute) { + return overviewRoute; + } + + const matchingComponent = components.find((component) => getComponentRoute(component.key) === normalizedHash); + return matchingComponent ? normalizedHash : overviewRoute; +} + +function navigate(route: string) { + window.location.hash = route; +} + +function useDocsRoute(components: AntdDocsComponent[]) { + const [route, setRoute] = useState(() => normalizeRoute(window.location.hash, components)); + + useEffect(() => { + const syncRoute = () => { + const nextRoute = normalizeRoute(window.location.hash, components); + setRoute(nextRoute); + + if ((window.location.hash || `#${overviewRoute}`) !== `#${nextRoute}`) { + window.location.hash = nextRoute; + } + }; + + syncRoute(); + window.addEventListener('hashchange', syncRoute); + + return () => { + window.removeEventListener('hashchange', syncRoute); + }; + }, [components]); + + return route; +} + +function ParameterTable({ parameters }: { parameters: AntdDocsParameter[] }) { + const normalizeValidValues = (validValues?: string | string[]) => { + if (typeof validValues === 'undefined') { + return []; + } + + return Array.isArray(validValues) ? validValues : [validValues]; + }; + + const columns: TableColumnsType = [ + { + dataIndex: 'name', + key: 'name', + render: (value: string) => {value}, + title: 'Parameter', + width: 180, + }, + { + dataIndex: 'type', + key: 'type', + render: (value?: string) => value ?? 'Object', + title: 'Type', + width: 140, + }, + { + dataIndex: 'required', + key: 'required', + render: (value?: boolean) => (value ? Required : Optional), + title: 'Requirement', + width: 140, + }, + { + dataIndex: 'description', + key: 'description', + render: (value?: string, record?: AntdDocsParameter) => { + const validValues = normalizeValidValues(record?.validValues); + + return ( + + {value ?? 'No description available.'} + {validValues.length > 0 ? ( + + {validValues.map((validValue) => ( + {validValue} + ))} + + ) : null} + + ); + }, + title: 'Description', + }, + ]; + + return ( + ({ ...parameter, key: parameter.name }))} + pagination={false} + size="middle" + /> + ); +} + +function OverviewPage({ components, overview, title }: { components: AntdDocsComponent[]; overview?: string; title?: string }) { + return ( +
+ + + + Generated from comment-based help + + {title ?? 'Ant Design Components'} + + {overview ?? + 'Browse the PowerShell Universal Ant Design framework components. Each component page is built from the command help so examples stay in sync with the docs and the module.'} + + + + +
+ {components.map((component) => ( + + + {component.category ?? 'Component'} + {component.title} + {component.summary ?? component.description} + + + + ))} +
+
+ ); +} + +function ComponentPage({ component }: { component: AntdDocsComponent }) { + return ( +
+ + + + {component.category ?? 'Component'} + {component.commandName ? }>{component.commandName} : null} + + {component.title} + {component.description ?? component.summary} + + {component.commandName ? {component.commandName} : null} + {component.sourceUrl ? ( + + ) : null} + + + + + {component.whenToUse && component.whenToUse.length > 0 ? ( + + + {component.whenToUse.map((item) => ( + {item} + ))} + + + ) : null} + + + {(component.examples ?? []).map((example) => ( + + +
+ {typeof example.preview === 'undefined' ? null : renderDescriptorNode(example.preview)} +
+ {example.description ? {example.description} : null} +
+                {example.code}
+              
+
+
+ ))} +
+ + + {component.parameters && component.parameters.length > 0 ? ( + + ) : ( + + )} + +
+ ); +} + +export function AntdDocs({ components = [], overview, title }: AntdDocsProps) { + const items = useMemo>(() => { + return [ + { + key: overviewRoute, + label: 'Overview', + }, + { + children: components.map((component) => ({ + key: getComponentRoute(component.key), + label: component.title, + })), + key: 'components', + label: 'Components', + type: 'group', + }, + ]; + }, [components]); + + const route = useDocsRoute(components); + const activeComponent = components.find((component) => getComponentRoute(component.key) === route); + + return ( +
+ +
+ {route === overviewRoute || !activeComponent ? ( + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdInput.tsx b/Apps/Frameworks/AntDesign/src/components/AntdInput.tsx new file mode 100644 index 0000000..afc028a --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdInput.tsx @@ -0,0 +1,375 @@ +import * as AntdIcons from '@ant-design/icons'; +import { Input as AntdInputControl } from 'antd'; +import type { InputProps as AntdInputComponentProps } from 'antd'; +import { + createElement, + useEffect, + useState, + type ChangeEvent, + type ComponentProps, + type ComponentType, + type CSSProperties, + type KeyboardEvent, + type ReactNode, +} from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdTextAreaComponentProps = ComponentProps; +type AntdSearchComponentProps = ComponentProps; +type AntdPasswordComponentProps = ComponentProps; +type AntdOtpComponentProps = ComponentProps; + +type AntdInputMode = 'input' | 'password' | 'otp' | 'search' | 'textarea'; +type AntdInputCountConfig = { + exceedFormatter?: 'truncate-graphemes'; + max?: number; + show?: boolean; + strategy?: 'graphemes'; +}; +type AntdOtpSeparatorConfig = { + evenColor?: string; + oddColor?: string; + type?: 'alternating-dash'; +}; + +type AntdInputProps = { + addonAfter?: DescriptorContent; + addonBefore?: DescriptorContent; + allowClear?: boolean; + autoComplete?: string; + autoFocus?: boolean; + autoSize?: boolean | { maxRows?: number; minRows?: number }; + className?: string; + classNames?: Record; + count?: AntdInputCountConfig; + dataAttributes?: Record; + defaultValue?: string; + disabled?: boolean; + enterButton?: boolean | DescriptorContent; + id?: string; + loading?: boolean; + maxLength?: number; + mode?: AntdInputMode; + name?: string; + onChange?: (data: { value: string }) => void; + onClear?: (data: { value: string }) => void; + onInput?: (data: { value: string[] }) => void; + onPressEnter?: (data: { value: string }) => void; + onSearch?: (data: { source?: string; value: string }) => void; + otpFormatter?: 'uppercase'; + otpLength?: number; + otpMask?: boolean | string; + otpSeparator?: DescriptorContent | AntdOtpSeparatorConfig; + passwordVisible?: boolean; + placeholder?: string; + prefix?: DescriptorContent; + render?: (component: DescriptorContent) => ReactNode; + rows?: number; + showCount?: boolean; + size?: AntdInputComponentProps['size']; + status?: AntdInputComponentProps['status']; + style?: CSSProperties; + styles?: Record; + suffix?: DescriptorContent; + type?: string; + value?: string; + variant?: AntdInputComponentProps['variant']; + visibilityToggle?: boolean; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +function resolveRenderableContent( + content: DescriptorContent | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode | undefined { + if (typeof content === 'undefined') { + return undefined; + } + + if (typeof content === 'string') { + const iconComponent = AntdIcons[content as keyof typeof AntdIcons]; + + if (iconComponent) { + return createElement(iconComponent as ComponentType); + } + } + + return renderDescriptorContent(content, render); +} + +function resolveCountConfig(count?: AntdInputCountConfig) { + if (!count) { + return undefined; + } + + return { + ...(typeof count.max === 'undefined' ? {} : { max: count.max }), + ...(typeof count.show === 'undefined' ? {} : { show: count.show }), + ...(count.strategy === 'graphemes' + ? { + strategy: (input: string) => Array.from(input).length, + } + : {}), + ...(count.exceedFormatter === 'truncate-graphemes' + ? { + exceedFormatter: (input: string, config: { max: number }) => Array.from(input).slice(0, config.max).join(''), + } + : {}), + }; +} + +function resolveOtpFormatter(formatter?: AntdInputProps['otpFormatter']) { + if (formatter === 'uppercase') { + return (value: string) => value.toUpperCase(); + } + + return undefined; +} + +function isOtpSeparatorConfig(value: DescriptorContent | AntdOtpSeparatorConfig): value is AntdOtpSeparatorConfig { + return typeof value === 'object' && value !== null && !Array.isArray(value) && value.type === 'alternating-dash'; +} + +function resolveOtpSeparator( + separator: DescriptorContent | AntdOtpSeparatorConfig | undefined, + render?: (component: DescriptorContent) => ReactNode, +) { + if (typeof separator === 'undefined') { + return undefined; + } + + if (isOtpSeparatorConfig(separator)) { + const evenColor = separator.evenColor ?? 'red'; + const oddColor = separator.oddColor ?? 'blue'; + + return (index: number) => -; + } + + return resolveRenderableContent(separator, render); +} + +function getEventValue(event: ChangeEvent): string; +function getEventValue(event: KeyboardEvent): string; +function getEventValue(event: { currentTarget?: { value?: string } }): string { + return event.currentTarget?.value ?? ''; +} + +export function AntdInput({ + addonAfter, + addonBefore, + allowClear, + autoComplete, + autoFocus, + autoSize, + className, + classNames, + count, + dataAttributes, + defaultValue, + disabled, + enterButton, + id, + loading, + maxLength, + mode = 'input', + name, + onChange, + onClear, + onInput, + onPressEnter, + onSearch, + otpFormatter, + otpLength, + otpMask, + otpSeparator, + passwordVisible, + placeholder, + prefix, + render, + rows, + showCount, + size, + status, + style, + styles, + suffix, + type, + value, + variant, + visibilityToggle, +}: AntdInputProps) { + const [currentValue, setCurrentValue] = useState(value ?? defaultValue ?? ''); + const [currentPasswordVisible, setCurrentPasswordVisible] = useState(passwordVisible ?? false); + + useEffect(() => { + if (typeof value !== 'undefined') { + setCurrentValue(value); + return; + } + + if (typeof defaultValue !== 'undefined') { + setCurrentValue(defaultValue); + } + }, [defaultValue, value]); + + useEffect(() => { + if (typeof passwordVisible !== 'undefined') { + setCurrentPasswordVisible(passwordVisible); + } + }, [passwordVisible]); + + const resolvedAddonAfter = resolveRenderableContent(addonAfter, render); + const resolvedAddonBefore = resolveRenderableContent(addonBefore, render); + const resolvedEnterButton = typeof enterButton === 'boolean' ? enterButton : resolveRenderableContent(enterButton, render); + const resolvedPrefix = resolveRenderableContent(prefix, render); + const resolvedSuffix = resolveRenderableContent(suffix, render); + const resolvedCount = resolveCountConfig(count); + const baseProps = { + ...(typeof autoComplete === 'undefined' ? {} : { autoComplete }), + ...(typeof autoFocus === 'undefined' ? {} : { autoFocus }), + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof classNames === 'undefined' ? {} : { classNames }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof maxLength === 'undefined' ? {} : { maxLength }), + ...(typeof name === 'undefined' ? {} : { name }), + ...(typeof placeholder === 'undefined' ? {} : { placeholder }), + ...(typeof showCount === 'undefined' ? {} : { showCount }), + ...(typeof size === 'undefined' ? {} : { size }), + ...(typeof status === 'undefined' ? {} : { status }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof styles === 'undefined' ? {} : { styles }), + ...(typeof variant === 'undefined' ? {} : { variant }), + ...(typeof resolvedCount === 'undefined' ? {} : { count: resolvedCount }), + ...toDataAttributeProps(dataAttributes), + }; + const inputChangeHandlers = { + onChange: (event: ChangeEvent) => { + const nextValue = getEventValue(event); + setCurrentValue(nextValue); + onChange?.({ value: nextValue }); + }, + onClear: () => { + setCurrentValue(''); + onClear?.({ value: '' }); + }, + onPressEnter: (event: KeyboardEvent) => { + onPressEnter?.({ value: getEventValue(event) }); + }, + }; + + if (mode === 'textarea') { + const textAreaProps: AntdTextAreaComponentProps = { + ...baseProps, + ...(typeof allowClear === 'undefined' ? {} : { allowClear }), + ...(typeof autoSize === 'undefined' ? {} : { autoSize }), + ...(typeof rows === 'undefined' ? {} : { rows }), + ...inputChangeHandlers, + value: currentValue, + }; + + return ; + } + + if (mode === 'search') { + const searchProps: AntdSearchComponentProps = { + ...baseProps, + ...(typeof allowClear === 'undefined' ? {} : { allowClear }), + ...(typeof resolvedAddonAfter === 'undefined' ? {} : { addonAfter: resolvedAddonAfter }), + ...(typeof resolvedAddonBefore === 'undefined' ? {} : { addonBefore: resolvedAddonBefore }), + ...(typeof resolvedEnterButton === 'undefined' ? {} : { enterButton: resolvedEnterButton }), + ...(typeof resolvedPrefix === 'undefined' ? {} : { prefix: resolvedPrefix }), + ...(typeof resolvedSuffix === 'undefined' ? {} : { suffix: resolvedSuffix }), + ...(typeof loading === 'undefined' ? {} : { loading }), + ...inputChangeHandlers, + onSearch: (searchValue, _event, info) => { + setCurrentValue(searchValue); + onSearch?.({ + value: searchValue, + ...(typeof info?.source === 'undefined' ? {} : { source: info.source }), + }); + }, + value: currentValue, + }; + + return ; + } + + if (mode === 'password') { + const passwordProps: AntdPasswordComponentProps = { + ...baseProps, + ...(typeof allowClear === 'undefined' ? {} : { allowClear }), + ...(typeof resolvedPrefix === 'undefined' ? {} : { prefix: resolvedPrefix }), + ...(typeof resolvedSuffix === 'undefined' ? {} : { suffix: resolvedSuffix }), + ...inputChangeHandlers, + ...(typeof visibilityToggle === 'undefined' && typeof passwordVisible === 'undefined' + ? {} + : { + visibilityToggle: typeof passwordVisible === 'undefined' + ? visibilityToggle + : { + visible: currentPasswordVisible, + onVisibleChange: (visible) => { + setCurrentPasswordVisible(visible); + }, + }, + }), + value: currentValue, + }; + + return ; + } + + if (mode === 'otp') { + const resolvedFormatter = resolveOtpFormatter(otpFormatter); + + const otpProps: AntdOtpComponentProps = { + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof resolvedFormatter === 'undefined' ? {} : { formatter: resolvedFormatter }), + ...(typeof otpLength === 'undefined' ? {} : { length: otpLength }), + ...(typeof otpMask === 'undefined' ? {} : { mask: otpMask }), + ...(typeof otpSeparator === 'undefined' ? {} : { separator: resolveOtpSeparator(otpSeparator, render) }), + ...(typeof size === 'undefined' ? {} : { size }), + ...(typeof status === 'undefined' ? {} : { status }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof variant === 'undefined' ? {} : { variant }), + onChange: (nextValue) => { + setCurrentValue(nextValue); + onChange?.({ value: nextValue }); + }, + onInput: (nextValue) => { + onInput?.({ value: nextValue }); + }, + value: currentValue, + ...toDataAttributeProps(dataAttributes), + }; + + return ; + } + + const inputProps: AntdInputComponentProps = { + ...baseProps, + ...(typeof allowClear === 'undefined' ? {} : { allowClear }), + ...(typeof resolvedAddonAfter === 'undefined' ? {} : { addonAfter: resolvedAddonAfter }), + ...(typeof resolvedAddonBefore === 'undefined' ? {} : { addonBefore: resolvedAddonBefore }), + ...(typeof resolvedPrefix === 'undefined' ? {} : { prefix: resolvedPrefix }), + ...(typeof resolvedSuffix === 'undefined' ? {} : { suffix: resolvedSuffix }), + ...(typeof type === 'undefined' ? {} : { type }), + ...inputChangeHandlers, + value: currentValue, + }; + + return ; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdLayout.tsx b/Apps/Frameworks/AntDesign/src/components/AntdLayout.tsx new file mode 100644 index 0000000..2b37626 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdLayout.tsx @@ -0,0 +1,127 @@ +import { Layout } from 'antd'; +import type { CSSProperties, ComponentProps, ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdBaseLayoutProps = { + className?: string; + content?: DescriptorContent; + dataAttributes?: Record; + id?: string; + render?: (component: DescriptorContent) => ReactNode; + style?: CSSProperties; +}; + +type AntdLayoutProps = AntdBaseLayoutProps & { + hasSider?: boolean; +}; + +type AntdLayoutSiderProps = AntdBaseLayoutProps & { + breakpoint?: ComponentProps['breakpoint']; + collapsed?: boolean; + collapsedWidth?: ComponentProps['collapsedWidth']; + collapsible?: boolean; + defaultCollapsed?: boolean; + reverseArrow?: boolean; + theme?: ComponentProps['theme']; + trigger?: DescriptorContent; + width?: ComponentProps['width']; + zeroWidthTriggerStyle?: CSSProperties; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +function toBaseProps({ + className, + dataAttributes, + id, + style, +}: { + className: string | undefined; + dataAttributes: Record | undefined; + id: string | undefined; + style: CSSProperties | undefined; +}) { + return { + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof style === 'undefined' ? {} : { style }), + ...toDataAttributeProps(dataAttributes), + }; +} + +export function AntdLayout({ className, content, dataAttributes, hasSider, id, render, style }: AntdLayoutProps) { + const layoutProps: ComponentProps & { id?: string } = { + ...toBaseProps({ className, dataAttributes, id, style }), + ...(typeof hasSider === 'undefined' ? {} : { hasSider }), + }; + + return {renderDescriptorContent(content, render)}; +} + +export function AntdLayoutHeader({ className, content, dataAttributes, id, render, style }: AntdBaseLayoutProps) { + const headerProps: ComponentProps & { id?: string } = { + ...toBaseProps({ className, dataAttributes, id, style }), + }; + + return {renderDescriptorContent(content, render)}; +} + +export function AntdLayoutContent({ className, content, dataAttributes, id, render, style }: AntdBaseLayoutProps) { + const contentProps: ComponentProps & { id?: string } = { + ...toBaseProps({ className, dataAttributes, id, style }), + }; + + return {renderDescriptorContent(content, render)}; +} + +export function AntdLayoutFooter({ className, content, dataAttributes, id, render, style }: AntdBaseLayoutProps) { + const footerProps: ComponentProps & { id?: string } = { + ...toBaseProps({ className, dataAttributes, id, style }), + }; + + return {renderDescriptorContent(content, render)}; +} + +export function AntdLayoutSider({ + breakpoint, + className, + collapsed, + collapsedWidth, + collapsible, + content, + dataAttributes, + defaultCollapsed, + id, + render, + reverseArrow, + style, + theme, + trigger, + width, + zeroWidthTriggerStyle, +}: AntdLayoutSiderProps) { + const siderProps: ComponentProps & { id?: string } = { + ...toBaseProps({ className, dataAttributes, id, style }), + ...(typeof breakpoint === 'undefined' ? {} : { breakpoint }), + ...(typeof collapsed === 'undefined' ? {} : { collapsed }), + ...(typeof collapsedWidth === 'undefined' ? {} : { collapsedWidth }), + ...(typeof collapsible === 'undefined' ? {} : { collapsible }), + ...(typeof defaultCollapsed === 'undefined' ? {} : { defaultCollapsed }), + ...(typeof reverseArrow === 'undefined' ? {} : { reverseArrow }), + ...(typeof theme === 'undefined' ? {} : { theme }), + ...(typeof trigger === 'undefined' ? {} : { trigger: renderDescriptorContent(trigger, render) }), + ...(typeof width === 'undefined' ? {} : { width }), + ...(typeof zeroWidthTriggerStyle === 'undefined' ? {} : { zeroWidthTriggerStyle }), + }; + + return {renderDescriptorContent(content, render)}; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdRate.tsx b/Apps/Frameworks/AntDesign/src/components/AntdRate.tsx new file mode 100644 index 0000000..2823c3a --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdRate.tsx @@ -0,0 +1,105 @@ +import * as AntdIcons from '@ant-design/icons'; +import { Rate as AntdRateControl } from 'antd'; +import type { RateProps as AntdRateComponentProps } from 'antd'; +import { createElement, type ComponentType, type CSSProperties, type ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdRateProps = { + allowClear?: boolean; + allowHalf?: boolean; + character?: DescriptorContent; + className?: string; + count?: number; + dataAttributes?: Record; + defaultValue?: number; + disabled?: boolean; + id?: string; + keyboard?: boolean; + onChange?: (data: { value: number }) => void; + onHoverChange?: (data: { value: number }) => void; + render?: (component: DescriptorContent) => ReactNode; + size?: 'small' | 'medium' | 'large'; + style?: CSSProperties; + tooltips?: string[]; + value?: number; +}; + +const rateFontSizeBySize: Record, number> = { + small: 15, + medium: 20, + large: 25, +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +function resolveCharacter( + character: DescriptorContent | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode | undefined { + if (typeof character === 'undefined') { + return undefined; + } + + if (typeof character === 'string') { + const iconComponent = AntdIcons[character as keyof typeof AntdIcons]; + + if (iconComponent) { + return createElement(iconComponent as ComponentType); + } + } + + return renderDescriptorContent(character, render); +} + +export function AntdRate({ + allowClear, + allowHalf, + character, + className, + count, + dataAttributes, + defaultValue, + disabled, + id, + keyboard, + onChange, + onHoverChange, + render, + size, + style, + tooltips, + value, +}: AntdRateProps) { + const resolvedCharacter = resolveCharacter(character, render); + const resolvedStyle = typeof size === 'undefined' ? style : { fontSize: rateFontSizeBySize[size], ...style }; + const rateProps: AntdRateComponentProps & { id?: string } = { + ...(typeof allowClear === 'undefined' ? {} : { allowClear }), + ...(typeof allowHalf === 'undefined' ? {} : { allowHalf }), + ...(typeof resolvedCharacter === 'undefined' ? {} : { character: resolvedCharacter }), + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof count === 'undefined' ? {} : { count }), + ...(typeof defaultValue === 'undefined' ? {} : { defaultValue }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof keyboard === 'undefined' ? {} : { keyboard }), + ...(typeof resolvedStyle === 'undefined' ? {} : { style: resolvedStyle }), + ...(typeof tooltips === 'undefined' ? {} : { tooltips }), + ...(typeof value === 'undefined' ? {} : { value }), + ...(typeof onChange === 'undefined' ? {} : { onChange: (nextValue) => onChange({ value: nextValue }) }), + ...(typeof onHoverChange === 'undefined' + ? {} + : { onHoverChange: (nextValue) => onHoverChange({ value: nextValue }) }), + ...toDataAttributeProps(dataAttributes), + }; + + return ; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdRow.tsx b/Apps/Frameworks/AntDesign/src/components/AntdRow.tsx new file mode 100644 index 0000000..d4fc6a4 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdRow.tsx @@ -0,0 +1,54 @@ +import { Row } from 'antd'; +import type { RowProps as AntdRowComponentProps } from 'antd'; +import type { CSSProperties, ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdRowProps = { + align?: AntdRowComponentProps['align']; + className?: string; + content?: DescriptorContent; + dataAttributes?: Record; + gutter?: AntdRowComponentProps['gutter']; + id?: string; + justify?: AntdRowComponentProps['justify']; + render?: (component: DescriptorContent) => ReactNode; + style?: CSSProperties; + wrap?: boolean; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +export function AntdRow({ + align, + className, + content, + dataAttributes, + gutter, + id, + justify, + render, + style, + wrap, +}: AntdRowProps) { + const rowProps: AntdRowComponentProps & { id?: string } = { + ...(typeof align === 'undefined' ? {} : { align }), + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof gutter === 'undefined' ? {} : { gutter }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof justify === 'undefined' ? {} : { justify }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof wrap === 'undefined' ? {} : { wrap }), + ...toDataAttributeProps(dataAttributes), + }; + + return {renderDescriptorContent(content, render)}; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdSwitch.tsx b/Apps/Frameworks/AntDesign/src/components/AntdSwitch.tsx new file mode 100644 index 0000000..ca91c58 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdSwitch.tsx @@ -0,0 +1,117 @@ +import * as AntdIcons from '@ant-design/icons'; +import { Switch as AntdSwitchControl } from 'antd'; +import type { SwitchProps as AntdSwitchComponentProps } from 'antd'; +import { createElement, useEffect, useState, type ComponentType, type CSSProperties, type ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdSwitchProps = { + checked?: boolean; + checkedChildren?: DescriptorContent; + className?: string; + classNames?: Record; + dataAttributes?: Record; + defaultChecked?: boolean; + disabled?: boolean; + id?: string; + loading?: boolean; + onChange?: (data: { checked: boolean; value: unknown }) => void; + onClick?: (data: { checked: boolean; value: unknown }) => void; + render?: (component: DescriptorContent) => ReactNode; + size?: AntdSwitchComponentProps['size']; + styles?: Record; + uncheckedChildren?: DescriptorContent; + value?: unknown; +}; + +type AntdSwitchRuntimeProps = AntdSwitchComponentProps & { + classNames?: Record; + styles?: Record; + id?: string; +}; + +function toDataAttributeProps(dataAttributes?: Record): Record { + if (!dataAttributes) { + return {}; + } + + return Object.fromEntries( + Object.entries(dataAttributes).map(([key, currentValue]) => [`data-${key}`, String(currentValue)]), + ); +} + +function resolveSwitchContent( + content: DescriptorContent | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode | undefined { + if (typeof content === 'undefined') { + return undefined; + } + + if (typeof content === 'string') { + const iconComponent = AntdIcons[content as keyof typeof AntdIcons]; + + if (iconComponent) { + return createElement(iconComponent as ComponentType); + } + } + + return renderDescriptorContent(content, render); +} + +export function AntdSwitch({ + checked, + checkedChildren, + className, + classNames, + dataAttributes, + defaultChecked, + disabled, + id, + loading, + onChange, + onClick, + render, + size, + styles, + uncheckedChildren, + value, +}: AntdSwitchProps) { + const [currentChecked, setCurrentChecked] = useState(checked ?? defaultChecked ?? false); + + useEffect(() => { + if (typeof checked !== 'undefined') { + setCurrentChecked(checked); + return; + } + + if (typeof defaultChecked !== 'undefined') { + setCurrentChecked(defaultChecked); + } + }, [checked, defaultChecked]); + + const switchProps: AntdSwitchRuntimeProps = { + checked: currentChecked, + ...(typeof checkedChildren === 'undefined' ? {} : { checkedChildren: resolveSwitchContent(checkedChildren, render) }), + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof classNames === 'undefined' ? {} : { classNames }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof loading === 'undefined' ? {} : { loading }), + ...(typeof size === 'undefined' ? {} : { size }), + ...(typeof styles === 'undefined' ? {} : { styles }), + ...(typeof uncheckedChildren === 'undefined' + ? {} + : { unCheckedChildren: resolveSwitchContent(uncheckedChildren, render) }), + onChange: (nextChecked) => { + setCurrentChecked(nextChecked); + onChange?.({ checked: nextChecked, value }); + }, + onClick: (nextChecked) => { + onClick?.({ checked: nextChecked, value }); + }, + ...toDataAttributeProps(dataAttributes), + }; + + return ; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/AntdText.tsx b/Apps/Frameworks/AntDesign/src/components/AntdText.tsx new file mode 100644 index 0000000..7c6115e --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdText.tsx @@ -0,0 +1,22 @@ +import { Card, Typography } from 'antd'; +import type { ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type AntdTextProps = { + content?: DescriptorContent; + id?: string; + render?: (component: DescriptorContent) => ReactNode; + text?: string; +}; + +export function AntdText({ content, id, render, text }: AntdTextProps) { + const resolvedContent = text ?? renderDescriptorContent(content, render); + const cardProps = typeof id === 'string' ? { id } : {}; + + return ( + + {resolvedContent} + + ); +} diff --git a/Apps/Frameworks/AntDesign/src/components/AntdTypography.tsx b/Apps/Frameworks/AntDesign/src/components/AntdTypography.tsx new file mode 100644 index 0000000..0a87730 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/AntdTypography.tsx @@ -0,0 +1,298 @@ +import * as AntdIcons from '@ant-design/icons'; +import { Typography } from 'antd'; +import { createElement, useEffect, useState, type ComponentProps, type ComponentType, type CSSProperties, type ElementType, type ReactNode } from 'react'; +import { renderDescriptorContent } from '../registry/renderDescriptor'; +import type { DescriptorContent } from '../types/dashboard'; + +type TypographyKind = 'text' | 'title' | 'paragraph' | 'link'; +type TypographyTone = ComponentProps['type']; +type TypographyTarget = ComponentProps['target']; +type EditableTrigger = 'icon' | 'text' | 'both'; + +type AntdTypographyCopyable = { + format?: 'text/plain' | 'text/html'; + icon?: DescriptorContent[]; + text?: string; + tooltips?: false | DescriptorContent[]; +}; + +type AntdTypographyEditable = { + enterIcon?: DescriptorContent | false; + icon?: DescriptorContent; + maxLength?: number; + text?: string; + tooltip?: DescriptorContent | false; + triggerType?: EditableTrigger[]; +}; + +type AntdTypographyEllipsis = { + defaultExpanded?: boolean; + expandable?: boolean; + rows?: number; + suffix?: string; + symbol?: DescriptorContent; + tooltip?: DescriptorContent | false; +}; + +type AntdTypographyProps = { + className?: string; + code?: boolean; + content?: DescriptorContent; + copyable?: boolean | AntdTypographyCopyable; + delete?: boolean; + disabled?: boolean; + editable?: boolean | AntdTypographyEditable; + ellipsis?: boolean | AntdTypographyEllipsis; + href?: string; + id?: string; + italic?: boolean; + keyboard?: boolean; + kind?: TypographyKind; + level?: 1 | 2 | 3 | 4 | 5; + mark?: boolean; + onChange?: (data: { value: string }) => void; + onClick?: (data: { value: unknown }) => void; + onCopy?: (data: { value: string }) => void; + onEditCancel?: (data: { value: string }) => void; + onEditEnd?: (data: { value: string }) => void; + onEditStart?: (data: { value: string }) => void; + onExpand?: (data: { expanded: boolean }) => void; + render?: (component: DescriptorContent) => ReactNode; + style?: CSSProperties; + strong?: boolean; + target?: TypographyTarget; + text?: string; + typographyType?: TypographyTone; + underline?: boolean; + value?: unknown; +}; + +function resolveIconOrContent( + value: DescriptorContent | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode | undefined { + if (typeof value === 'undefined') { + return undefined; + } + + if (typeof value === 'string') { + const iconComponent = AntdIcons[value as keyof typeof AntdIcons]; + + if (typeof iconComponent === 'function') { + return createElement(iconComponent as ComponentType); + } + } + + return renderDescriptorContent(value, render); +} + +function resolveNodeArray( + values: DescriptorContent[] | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode[] | undefined { + if (!values || values.length === 0) { + return undefined; + } + + return values.map((value, index) => {resolveIconOrContent(value, render)}); +} + +function resolveEditableTriggerType(triggerType?: EditableTrigger[]) { + if (!triggerType || triggerType.length === 0) { + return undefined; + } + + const resolvedTriggerTypes = new Set<'icon' | 'text'>(); + + for (const currentTrigger of triggerType) { + if (currentTrigger === 'both') { + resolvedTriggerTypes.add('icon'); + resolvedTriggerTypes.add('text'); + continue; + } + + resolvedTriggerTypes.add(currentTrigger); + } + + return Array.from(resolvedTriggerTypes); +} + +function getTypographyComponent(kind: TypographyKind) { + switch (kind) { + case 'title': + return Typography.Title; + case 'paragraph': + return Typography.Paragraph; + case 'link': + return Typography.Link; + default: + return Typography.Text; + } +} + +export function AntdTypography({ + className, + code, + content, + copyable, + delete: deleted, + disabled, + editable, + ellipsis, + href, + id, + italic, + keyboard, + kind = 'text', + level, + mark, + onChange, + onClick, + onCopy, + onEditCancel, + onEditEnd, + onEditStart, + onExpand, + render, + style, + strong, + target, + text, + typographyType, + underline, + value, +}: AntdTypographyProps) { + const resolvedContent = typeof text !== 'undefined' ? text : renderDescriptorContent(content, render); + const initialEditableText = typeof text === 'string' + ? text + : typeof resolvedContent === 'string' + ? resolvedContent + : ''; + const configuredEditableText = typeof editable === 'object' && typeof editable.text === 'string' + ? editable.text + : initialEditableText; + const [editableValue, setEditableValue] = useState(configuredEditableText); + + useEffect(() => { + setEditableValue(configuredEditableText); + }, [configuredEditableText]); + + const renderedContent = editable ? editableValue : resolvedContent; + const resolvedCopyable = (() => { + if (typeof copyable === 'undefined') { + return undefined; + } + + if (typeof copyable === 'boolean') { + return copyable; + } + + const tooltips = copyable.tooltips === false + ? false + : resolveNodeArray(copyable.tooltips, render); + const resolvedCopyText = copyable.text ?? (typeof renderedContent === 'string' ? renderedContent : initialEditableText); + + return { + ...(typeof copyable.format === 'undefined' ? {} : { format: copyable.format }), + ...(typeof copyable.icon === 'undefined' ? {} : { icon: resolveNodeArray(copyable.icon, render) }), + ...(typeof resolvedCopyText === 'undefined' ? {} : { text: resolvedCopyText }), + ...(typeof tooltips === 'undefined' ? {} : { tooltips }), + ...(typeof onCopy === 'undefined' + ? {} + : { + onCopy: () => onCopy({ value: resolvedCopyText ?? '' }), + }), + }; + })(); + const resolvedEditable = (() => { + if (typeof editable === 'undefined') { + return undefined; + } + + if (typeof editable === 'boolean') { + return editable; + } + + return { + ...(typeof editable.enterIcon === 'undefined' + ? {} + : { enterIcon: editable.enterIcon === false ? null : resolveIconOrContent(editable.enterIcon, render) }), + ...(typeof editable.icon === 'undefined' ? {} : { icon: resolveIconOrContent(editable.icon, render) }), + ...(typeof editable.maxLength === 'undefined' ? {} : { maxLength: editable.maxLength }), + ...(typeof editable.tooltip === 'undefined' + ? {} + : { tooltip: editable.tooltip === false ? false : resolveIconOrContent(editable.tooltip, render) }), + ...(typeof editable.text === 'undefined' ? {} : { text: editableValue }), + ...(typeof editable.triggerType === 'undefined' + ? {} + : { triggerType: resolveEditableTriggerType(editable.triggerType) }), + onCancel: () => onEditCancel?.({ value: editableValue }), + onChange: (nextValue: string) => { + setEditableValue(nextValue); + onChange?.({ value: nextValue }); + }, + onEnd: () => onEditEnd?.({ value: editableValue }), + onStart: () => onEditStart?.({ value: editableValue }), + }; + })(); + const resolvedEllipsis = (() => { + if (typeof ellipsis === 'undefined') { + return undefined; + } + + if (typeof ellipsis === 'boolean') { + return ellipsis; + } + + return { + ...(typeof ellipsis.defaultExpanded === 'undefined' ? {} : { defaultExpanded: ellipsis.defaultExpanded }), + ...(typeof ellipsis.expandable === 'undefined' ? {} : { expandable: ellipsis.expandable }), + ...(typeof ellipsis.rows === 'undefined' ? {} : { rows: ellipsis.rows }), + ...(typeof ellipsis.suffix === 'undefined' ? {} : { suffix: ellipsis.suffix }), + ...(typeof ellipsis.symbol === 'undefined' ? {} : { symbol: resolveIconOrContent(ellipsis.symbol, render) }), + ...(typeof ellipsis.tooltip === 'undefined' + ? {} + : { tooltip: ellipsis.tooltip === false ? false : resolveIconOrContent(ellipsis.tooltip, render) }), + ...(typeof onExpand === 'undefined' + ? {} + : { + onExpand: (_event: MouseEvent, info: { expanded: boolean }) => onExpand({ expanded: info.expanded }), + }), + }; + })(); + + const typographyProps: Record = { + ...(typeof className === 'undefined' ? {} : { className }), + ...(typeof code === 'undefined' ? {} : { code }), + ...(typeof deleted === 'undefined' ? {} : { delete: deleted }), + ...(typeof disabled === 'undefined' ? {} : { disabled }), + ...(typeof id === 'undefined' ? {} : { id }), + ...(typeof italic === 'undefined' ? {} : { italic }), + ...(typeof keyboard === 'undefined' ? {} : { keyboard }), + ...(typeof mark === 'undefined' ? {} : { mark }), + ...(typeof style === 'undefined' ? {} : { style }), + ...(typeof strong === 'undefined' ? {} : { strong }), + ...(typeof typographyType === 'undefined' ? {} : { type: typographyType }), + ...(typeof underline === 'undefined' ? {} : { underline }), + ...(typeof resolvedCopyable === 'undefined' ? {} : { copyable: resolvedCopyable }), + ...(typeof resolvedEditable === 'undefined' ? {} : { editable: resolvedEditable }), + ...(typeof resolvedEllipsis === 'undefined' ? {} : { ellipsis: resolvedEllipsis }), + ...(typeof onClick === 'undefined' ? {} : { onClick: () => onClick({ value }) }), + }; + + if (kind === 'title' && typeof level !== 'undefined') { + typographyProps.level = level; + } + + if (kind === 'link') { + if (typeof href !== 'undefined') { + typographyProps.href = href; + } + + if (typeof target !== 'undefined') { + typographyProps.target = target; + } + } + + return createElement(getTypographyComponent(kind) as ElementType, typographyProps, renderedContent); +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/components/UnknownComponent.tsx b/Apps/Frameworks/AntDesign/src/components/UnknownComponent.tsx new file mode 100644 index 0000000..1f3ac5d --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/components/UnknownComponent.tsx @@ -0,0 +1,13 @@ +import { Alert } from 'antd'; +import type { DashboardDescriptor } from '../types/dashboard'; + +export function UnknownComponent({ id, type }: DashboardDescriptor) { + return ( + + ); +} diff --git a/Apps/Frameworks/AntDesign/src/config/colorMode.ts b/Apps/Frameworks/AntDesign/src/config/colorMode.ts new file mode 100644 index 0000000..4c5bf49 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/config/colorMode.ts @@ -0,0 +1,51 @@ +export type ColorModePreference = 'system' | 'light' | 'dark'; +export type ResolvedColorMode = 'light' | 'dark'; + +const COLOR_MODE_STORAGE_KEY = 'ud-color-mode'; +const COLOR_MODE_MEDIA_QUERY = '(prefers-color-scheme: dark)'; + +function isColorModePreference(value: string | null): value is ColorModePreference { + return value === 'system' || value === 'light' || value === 'dark'; +} + +export function readStoredColorModePreference(): ColorModePreference { + try { + const storedPreference = window.localStorage.getItem(COLOR_MODE_STORAGE_KEY); + return isColorModePreference(storedPreference) ? storedPreference : 'system'; + } catch { + return 'system'; + } +} + +export function persistColorModePreference(preference: ColorModePreference): void { + try { + if (preference === 'system') { + window.localStorage.removeItem(COLOR_MODE_STORAGE_KEY); + return; + } + + window.localStorage.setItem(COLOR_MODE_STORAGE_KEY, preference); + } catch { + // Ignore storage access failures and keep the current in-memory preference. + } +} + +export function readSystemColorMode(): ResolvedColorMode { + return window.matchMedia(COLOR_MODE_MEDIA_QUERY).matches ? 'dark' : 'light'; +} + +export function resolveColorMode( + preference: ColorModePreference, + systemColorMode: ResolvedColorMode, +): ResolvedColorMode { + return preference === 'system' ? systemColorMode : preference; +} + +export function applyResolvedColorMode(colorMode: ResolvedColorMode): void { + document.documentElement.dataset.colorMode = colorMode; + document.documentElement.style.colorScheme = colorMode; +} + +export function getColorModeMediaQuery(): string { + return COLOR_MODE_MEDIA_QUERY; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/config/runtime.ts b/Apps/Frameworks/AntDesign/src/config/runtime.ts new file mode 100644 index 0000000..f010591 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/config/runtime.ts @@ -0,0 +1,30 @@ +export type RuntimeMetadata = { + baseUrl: string; + dashboardId: string | null; + location: URL; + timezone: string; +}; + +function getMetaContent(name: string): string | null { + return document.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? null; +} + +function normalizeBaseUrl(baseUrl: string | null): string { + if (!baseUrl || baseUrl === '/') { + return ''; + } + + return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; +} + +export function getRuntimeMetadata(): RuntimeMetadata { + const location = new URL(window.location.href); + const dashboardId = window.localStorage.getItem('ud-dashboard') ?? getMetaContent('ud-dashboard'); + + return { + baseUrl: normalizeBaseUrl(getMetaContent('baseurl')), + dashboardId, + location, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; +} diff --git a/Apps/Frameworks/AntDesign/src/features/bootstrap/useBootstrap.ts b/Apps/Frameworks/AntDesign/src/features/bootstrap/useBootstrap.ts new file mode 100644 index 0000000..470c1f5 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/features/bootstrap/useBootstrap.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchDashboardBootstrap } from '../../transport/dashboardTransport'; +import { useRuntimeStore } from '../../state/runtimeStore'; + +export function useBootstrap() { + const baseUrl = useRuntimeStore((state) => state.baseUrl); + + return useQuery({ + queryKey: ['dashboard-bootstrap', baseUrl], + queryFn: () => fetchDashboardBootstrap(baseUrl), + staleTime: Number.POSITIVE_INFINITY, + }); +} diff --git a/Apps/Frameworks/AntDesign/src/features/transport/useDashboardConnection.ts b/Apps/Frameworks/AntDesign/src/features/transport/useDashboardConnection.ts new file mode 100644 index 0000000..3218543 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/features/transport/useDashboardConnection.ts @@ -0,0 +1,99 @@ +import { useEffect } from 'react'; +import { createDashboardHubConnection } from '../../transport/dashboardTransport'; +import { dispatchIncomingHubMessage, setElementEventPublisher } from '../../registry/universalDashboard'; +import { useRuntimeStore } from '../../state/runtimeStore'; + +const incomingHubMessages = [ + 'antdesign-message', + 'setState', + 'requestState', + 'addElement', + 'clearElement', + 'removeElement', + 'syncElement', + 'download', + 'redirect', + 'log', + 'write', +] as const; + +export function useDashboardConnection() { + const baseUrl = useRuntimeStore((state) => state.baseUrl); + const dashboardId = useRuntimeStore((state) => state.dashboardId); + const pageId = useRuntimeStore((state) => state.pageId); + const sessionId = useRuntimeStore((state) => state.sessionId); + const timezone = useRuntimeStore((state) => state.timezone); + const setConnectionId = useRuntimeStore((state) => state.setConnectionId); + const setConnectionStatus = useRuntimeStore((state) => state.setConnectionStatus); + + useEffect(() => { + if (!dashboardId || !pageId || !sessionId) { + return; + } + + const connection = createDashboardHubConnection({ + baseUrl, + dashboardId, + pageId, + sessionId, + timezone, + }); + + setElementEventPublisher(async (payload) => { + await connection.invoke('event', payload); + }); + + for (const messageType of incomingHubMessages) { + connection.on(messageType, (payload?: unknown) => { + dispatchIncomingHubMessage(messageType, payload); + }); + } + + setConnectionStatus('connecting'); + setConnectionId(null); + + connection.onreconnecting((error) => { + setConnectionId(null); + setConnectionStatus('reconnecting', error?.message ?? null); + }); + + connection.onreconnected((connectionId) => { + setConnectionId(connectionId ?? connection.connectionId ?? null); + setConnectionStatus('connected'); + }); + + connection.onclose((error) => { + setConnectionId(null); + setConnectionStatus('disconnected', error?.message ?? null); + }); + + let isDisposed = false; + + void connection.start().then( + () => { + if (!isDisposed) { + setConnectionId(connection.connectionId ?? null); + setConnectionStatus('connected'); + } + }, + (error: unknown) => { + if (!isDisposed) { + setConnectionId(null); + const message = error instanceof Error ? error.message : 'SignalR connection failed.'; + setConnectionStatus('disconnected', message); + } + }, + ); + + return () => { + isDisposed = true; + setElementEventPublisher(null); + setConnectionId(null); + for (const messageType of incomingHubMessages) { + connection.off(messageType); + } + setConnectionStatus('disconnected'); + void connection.stop(); + }; + }, [baseUrl, dashboardId, pageId, sessionId, setConnectionId, setConnectionStatus, timezone]); +} diff --git a/Apps/Frameworks/AntDesign/src/main.tsx b/Apps/Frameworks/AntDesign/src/main.tsx new file mode 100644 index 0000000..03eb761 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/main.tsx @@ -0,0 +1,34 @@ +import 'antd/dist/reset.css'; +import './app/app.css'; +import * as React from 'react'; +import ReactDOM from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { App } from './app/App'; +import { + applyResolvedColorMode, + readStoredColorModePreference, + readSystemColorMode, + resolveColorMode, +} from './config/colorMode'; +import { getRuntimeMetadata } from './config/runtime'; +import { ensureUniversalDashboardGlobal } from './registry/universalDashboard'; +import { registerBuiltins } from './registry/registerBuiltins'; +import { useRuntimeStore } from './state/runtimeStore'; + +const queryClient = new QueryClient(); + +Object.assign(globalThis as Record, { + React, + react: React, +}); + +ensureUniversalDashboardGlobal(); +registerBuiltins(); +useRuntimeStore.getState().initializeShell(getRuntimeMetadata()); +applyResolvedColorMode(resolveColorMode(readStoredColorModePreference(), readSystemColorMode())); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , +); diff --git a/Apps/Frameworks/AntDesign/src/registry/components.ts b/Apps/Frameworks/AntDesign/src/registry/components.ts new file mode 100644 index 0000000..3f6bbcf --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/registry/components.ts @@ -0,0 +1,13 @@ +import type { ComponentType } from 'react'; + +export type RegisteredDashboardComponent = ComponentType>; + +const componentRegistry = new Map(); + +export function registerComponent(type: string, component: RegisteredDashboardComponent) { + componentRegistry.set(type, component); +} + +export function getRegisteredComponent(type: string) { + return componentRegistry.get(type); +} diff --git a/Apps/Frameworks/AntDesign/src/registry/registerBuiltins.tsx b/Apps/Frameworks/AntDesign/src/registry/registerBuiltins.tsx new file mode 100644 index 0000000..c1ab60f --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/registry/registerBuiltins.tsx @@ -0,0 +1,66 @@ +import { lazy, type ComponentType } from 'react'; +import { withComponentFeatures } from 'universal-dashboard'; +import { registerComponent, type RegisteredDashboardComponent } from './components'; + +let isRegistered = false; + +type DashboardComponentModule = Record; + +function createLazyComponent( + loader: () => Promise<{ default: ComponentType> }>, +): RegisteredDashboardComponent { + const LazyComponent = lazy(loader); + + return function LazyDashboardComponent(props: Record) { + return ; + }; +} + +function loadWrappedComponent( + loader: () => Promise, + exportName: string, +): () => Promise<{ default: ComponentType> }> { + return async () => { + const module = await loader(); + const component = module[exportName]; + + if (!component) { + throw new Error(`Missing component export '${exportName}'.`); + } + + return { + default: withComponentFeatures(component as RegisteredDashboardComponent) as ComponentType>, + }; + }; +} + +function registerLazyComponent( + type: string, + loader: () => Promise, + exportName: string, +) { + registerComponent(type, createLazyComponent(loadWrappedComponent(loader, exportName))); +} + +export function registerBuiltins() { + if (isRegistered) { + return; + } + + registerLazyComponent('antd-button', () => import('../components/AntdButton'), 'AntdButton'); + registerLazyComponent('antd-checkbox', () => import('../components/AntdCheckbox'), 'AntdCheckbox'); + registerLazyComponent('antd-col', () => import('../components/AntdCol'), 'AntdCol'); + registerLazyComponent('antd-docs', () => import('../components/AntdDocs'), 'AntdDocs'); + registerLazyComponent('antd-input', () => import('../components/AntdInput'), 'AntdInput'); + registerLazyComponent('antd-layout', () => import('../components/AntdLayout'), 'AntdLayout'); + registerLazyComponent('antd-layout-content', () => import('../components/AntdLayout'), 'AntdLayoutContent'); + registerLazyComponent('antd-layout-footer', () => import('../components/AntdLayout'), 'AntdLayoutFooter'); + registerLazyComponent('antd-layout-header', () => import('../components/AntdLayout'), 'AntdLayoutHeader'); + registerLazyComponent('antd-layout-sider', () => import('../components/AntdLayout'), 'AntdLayoutSider'); + registerLazyComponent('antd-rate', () => import('../components/AntdRate'), 'AntdRate'); + registerLazyComponent('antd-row', () => import('../components/AntdRow'), 'AntdRow'); + registerLazyComponent('antd-switch', () => import('../components/AntdSwitch'), 'AntdSwitch'); + registerLazyComponent('antd-text', () => import('../components/AntdText'), 'AntdText'); + registerLazyComponent('antd-typography', () => import('../components/AntdTypography'), 'AntdTypography'); + isRegistered = true; +} diff --git a/Apps/Frameworks/AntDesign/src/registry/renderDescriptor.tsx b/Apps/Frameworks/AntDesign/src/registry/renderDescriptor.tsx new file mode 100644 index 0000000..dc9aad7 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/registry/renderDescriptor.tsx @@ -0,0 +1,57 @@ +import { Fragment, Suspense, isValidElement, type ReactNode } from 'react'; +import { getRegisteredComponent } from './components'; +import { UnknownComponent } from '../components/UnknownComponent'; +import type { DashboardDescriptor, DescriptorContent, DashboardPrimitive } from '../types/dashboard'; + +function isDashboardDescriptor(value: DescriptorContent): value is DashboardDescriptor { + return typeof value === 'object' && value !== null && !Array.isArray(value) && 'type' in value; +} + +function renderPrimitive(value: DashboardPrimitive): ReactNode { + if (value === null) { + return null; + } + + return value; +} + +export function renderDescriptorContent( + content: DescriptorContent | undefined, + render?: (component: DescriptorContent) => ReactNode, +): ReactNode { + if (typeof content === 'undefined') { + return null; + } + + if (render) { + return render(content); + } + + return renderDescriptorNode(content); +} + +export function renderDescriptorNode(node: DescriptorContent): ReactNode { + if (Array.isArray(node)) { + return node.map((item, index) => {renderDescriptorNode(item)}); + } + + if (!isDashboardDescriptor(node)) { + return renderPrimitive(node); + } + + const Component = getRegisteredComponent(node.type); + + if (!Component) { + return ; + } + + return ( + + )} /> + + ); +} + +export function renderExistingNode(node: ReactNode): ReactNode { + return isValidElement(node) ? node : null; +} diff --git a/Apps/Frameworks/AntDesign/src/registry/universalDashboard.tsx b/Apps/Frameworks/AntDesign/src/registry/universalDashboard.tsx new file mode 100644 index 0000000..a9cbc04 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/registry/universalDashboard.tsx @@ -0,0 +1,321 @@ +import { message as antMessage } from 'antd'; +import type { ReactNode } from 'react'; +import { registerComponent, type RegisteredDashboardComponent } from './components'; +import { renderDescriptorNode } from './renderDescriptor'; +import { startDashboardDownload } from '../transport/dashboardTransport'; +import { useRuntimeStore } from '../state/runtimeStore'; + +type SubscriptionCallback = (topic: string, payload: Record) => void; + +type ResponseCallback = (payload: unknown) => void; + +type RequestHeaders = Record; + +type UniversalDashboardGlobal = { + register: (type: string, component: RegisteredDashboardComponent) => void; + renderComponent: (component: unknown, history?: unknown) => ReactNode; + subscribe: (topic: string, callback: SubscriptionCallback) => number; + unsubscribe: (token: number) => void; + publish: (topic: string, payload: unknown) => void; + post: (path: string, body: unknown, callback?: ResponseCallback) => Promise; + postWithHeaders: ( + path: string, + body: unknown, + callback?: ResponseCallback, + headers?: RequestHeaders, + ) => Promise; + get: (path: string, callback?: ResponseCallback) => Promise; +}; + +declare global { + interface Window { + UniversalDashboard?: UniversalDashboardGlobal; + } +} + +type Subscription = { + topic: string; + callback: SubscriptionCallback; +}; + +type EventPublisher = (payload: unknown) => Promise; + +const subscriptions = new Map(); +let nextSubscriptionToken = 1; +let publishElementEvent: EventPublisher | null = null; +let globalInitialized = false; + +function buildUrl(baseUrl: string, path: string): string { + return `${baseUrl}${path}`; +} + +function createRequestBody(body: unknown, headers?: Headers): BodyInit | undefined { + if (typeof body === 'undefined' || body === null) { + return undefined; + } + + if (body instanceof FormData) { + return body; + } + + if (typeof body === 'string') { + if (headers && !headers.has('Content-Type')) { + headers.set('Content-Type', 'text/plain'); + } + + return body; + } + + if (headers && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json'); + } + + return JSON.stringify(body); +} + +async function parseResponse(response: Response): Promise { + const contentType = response.headers.get('content-type') ?? ''; + + if (!response.ok) { + const errorBody = contentType.includes('application/json') + ? JSON.stringify(await response.json()) + : await response.text(); + throw new Error(`UniversalDashboard request failed with status ${response.status}: ${errorBody}`); + } + + if (response.status === 204) { + return null; + } + + if (contentType.includes('application/json')) { + return response.json(); + } + + return response.text(); +} + +async function request( + method: 'GET' | 'POST', + path: string, + body?: unknown, + callback?: ResponseCallback, + headers?: RequestHeaders, +): Promise { + const baseUrl = useRuntimeStore.getState().baseUrl; + const requestHeaders = new Headers(headers); + + if ( + method === 'POST' + && path.startsWith('/api/internal/component/element/') + && !requestHeaders.has('UDConnectionId') + ) { + const connectionId = useRuntimeStore.getState().connectionId; + if (connectionId) { + requestHeaders.set('UDConnectionId', connectionId); + } + } + + const requestBody = method === 'GET' ? undefined : createRequestBody(body, requestHeaders); + const requestInit: RequestInit = { + method, + credentials: 'include', + headers: requestHeaders, + }; + + if (requestBody !== undefined) { + requestInit.body = requestBody; + } + + const response = await fetch(buildUrl(baseUrl, path), requestInit); + const payload = await parseResponse(response); + callback?.(payload); + return payload; +} + +function notifySubscribers(topic: string, payload: Record) { + for (const subscription of subscriptions.values()) { + if (subscription.topic === topic) { + subscription.callback(topic, payload); + } + } +} + +function resolveTopic(payload: unknown): string | null { + if (typeof payload === 'string' && payload.length > 0) { + return payload; + } + + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return null; + } + + const record = payload as Record; + const candidate = record.componentId ?? record.id ?? record.elementId ?? record.targetId; + return typeof candidate === 'string' && candidate.length > 0 ? candidate : null; +} + +function normalizePayload(messageType: string, payload: unknown): Record { + if (payload && typeof payload === 'object' && !Array.isArray(payload)) { + return { + type: messageType, + ...(payload as Record), + }; + } + + return { + type: messageType, + value: payload, + }; +} + +function handleDownload(payload: unknown) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return; + } + + const dashboardId = useRuntimeStore.getState().dashboardId; + if (!dashboardId) { + return; + } + + const record = payload as Record; + const downloadId = typeof record.id === 'string' ? record.id : null; + const fileName = typeof record.fileName === 'string' ? record.fileName : undefined; + + if (!downloadId) { + return; + } + + startDashboardDownload(useRuntimeStore.getState().baseUrl, dashboardId, downloadId, fileName); +} + +function handleRedirect(payload: unknown) { + const target = typeof payload === 'string' + ? payload + : payload && typeof payload === 'object' && !Array.isArray(payload) + ? (payload as Record).url ?? (payload as Record).location + : null; + + if (typeof target === 'string' && target.length > 0) { + window.location.assign(target); + } +} + +function handleAntDesignMessage(payload: unknown) { + if (typeof payload === 'string' && payload.length > 0) { + void antMessage.info(payload); + return; + } + + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return; + } + + const record = payload as Record; + const content = typeof record.content === 'string' + ? record.content + : typeof record.message === 'string' + ? record.message + : null; + + if (!content) { + return; + } + + const duration = typeof record.duration === 'number' ? record.duration : undefined; + const key = typeof record.key === 'string' || typeof record.key === 'number' ? record.key : undefined; + const type = typeof record.type === 'string' ? record.type : 'info'; + const options = { + content, + ...(typeof duration === 'number' ? { duration } : {}), + ...(typeof key !== 'undefined' ? { key } : {}), + }; + + switch (type) { + case 'success': + void antMessage.success(options); + return; + case 'warning': + void antMessage.warning(options); + return; + case 'error': + void antMessage.error(options); + return; + case 'loading': + void antMessage.loading(options); + return; + default: + void antMessage.info(options); + return; + } +} + +export function setElementEventPublisher(publisher: EventPublisher | null) { + publishElementEvent = publisher; +} + +export function dispatchIncomingHubMessage(messageType: string, payload?: unknown) { + switch (messageType) { + case 'antdesign-message': + handleAntDesignMessage(payload); + return; + case 'download': + handleDownload(payload); + return; + case 'redirect': + handleRedirect(payload); + return; + case 'write': + case 'log': + if (typeof payload !== 'undefined') { + console.info(`[${messageType}]`, payload); + } + return; + default: + break; + } + + const topic = resolveTopic(payload); + if (!topic) { + return; + } + + notifySubscribers(topic, normalizePayload(messageType, payload)); +} + +export function ensureUniversalDashboardGlobal() { + if (globalInitialized) { + return; + } + + window.UniversalDashboard = { + register: (type: string, component: RegisteredDashboardComponent) => { + registerComponent(type, component); + }, + renderComponent: (component: unknown) => renderDescriptorNode(component as never), + subscribe: (topic: string, callback: SubscriptionCallback) => { + const token = nextSubscriptionToken; + nextSubscriptionToken += 1; + subscriptions.set(token, { topic, callback }); + return token; + }, + unsubscribe: (token: number) => { + subscriptions.delete(token); + }, + publish: (topic: string, payload: unknown) => { + if (topic === 'element-event' && publishElementEvent) { + void publishElementEvent(payload); + } + }, + post: (path: string, body: unknown, callback?: ResponseCallback) => request('POST', path, body, callback), + postWithHeaders: ( + path: string, + body: unknown, + callback?: ResponseCallback, + headers?: RequestHeaders, + ) => request('POST', path, body, callback, headers), + get: (path: string, callback?: ResponseCallback) => request('GET', path, undefined, callback), + }; + + globalInitialized = true; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/src/schema/dashboard.ts b/Apps/Frameworks/AntDesign/src/schema/dashboard.ts new file mode 100644 index 0000000..3659074 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/schema/dashboard.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; +import type { DashboardBootstrap, EndpointDescriptor } from '../types/dashboard'; + +export const endpointDescriptorSchema = z + .object({ + name: z.string(), + javaScript: z.string().optional(), + websocket: z.boolean().optional(), + accept: z.string().optional(), + contentType: z.string().optional(), + }) + .catchall(z.unknown()) satisfies z.ZodType; + +export const dashboardDescriptorSchema = z + .object({ + type: z.string(), + id: z.string().optional(), + content: z.unknown().optional(), + isPlugin: z.boolean().optional(), + assetId: z.string().optional(), + }) + .catchall(z.unknown()); + +export const descriptorContentSchema: z.ZodTypeAny = z.lazy((): z.ZodTypeAny => + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + dashboardDescriptorSchema, + z.array(descriptorContentSchema), + ]), +); + +export const dashboardBootstrapSchema = z.object({ + dashboard: descriptorContentSchema, + sessionId: z.string(), + pageId: z.string(), + authType: z.string().optional(), + roles: z.array(z.string()).optional(), + user: z.string().optional(), + idleTimeout: z.number().optional(), + dashboardName: z.string().optional(), + developerLicense: z.boolean().optional(), +}); + +export function parseDashboardBootstrap(payload: unknown): DashboardBootstrap { + return dashboardBootstrapSchema.parse(payload) as DashboardBootstrap; +} diff --git a/Apps/Frameworks/AntDesign/src/state/runtimeStore.ts b/Apps/Frameworks/AntDesign/src/state/runtimeStore.ts new file mode 100644 index 0000000..29d5029 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/state/runtimeStore.ts @@ -0,0 +1,75 @@ +import { create } from 'zustand'; +import type { RuntimeMetadata } from '../config/runtime'; +import type { DashboardBootstrap, DescriptorContent } from '../types/dashboard'; + +export type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'; + +type RuntimeStore = RuntimeMetadata & { + connectionId: string | null; + connectionStatus: ConnectionStatus; + descriptorTree: DescriptorContent | null; + componentState: Record>; + dashboardName: string | null; + roles: string[]; + sessionId: string | null; + pageId: string | null; + transportError: string | null; + initializeShell: (metadata: RuntimeMetadata) => void; + setBootstrap: (bootstrap: DashboardBootstrap) => void; + setConnectionId: (connectionId: string | null) => void; + setConnectionStatus: (status: ConnectionStatus, error?: string | null) => void; + setComponentState: (componentId: string, state: Record) => void; + getComponentState: (componentId: string) => Record | null; +}; + +const initialMetadata: RuntimeMetadata = { + baseUrl: '', + dashboardId: null, + location: new URL('http://localhost'), + timezone: 'UTC', +}; + +export const useRuntimeStore = create((set, get) => ({ + ...initialMetadata, + connectionId: null, + connectionStatus: 'idle', + descriptorTree: null, + componentState: {}, + dashboardName: null, + roles: [], + sessionId: null, + pageId: null, + transportError: null, + initializeShell: (metadata) => { + set(metadata); + }, + setBootstrap: (bootstrap) => { + set({ + descriptorTree: bootstrap.dashboard, + dashboardName: bootstrap.dashboardName ?? null, + pageId: bootstrap.pageId, + roles: bootstrap.roles ?? [], + sessionId: bootstrap.sessionId, + }); + }, + setConnectionId: (connectionId) => { + set({ connectionId }); + }, + setConnectionStatus: (status, error = null) => { + set({ + connectionStatus: status, + transportError: error, + }); + }, + setComponentState: (componentId, state) => { + set((current) => ({ + componentState: { + ...current.componentState, + [componentId]: state, + }, + })); + }, + getComponentState: (componentId) => { + return get().componentState[componentId] ?? null; + }, +})); diff --git a/Apps/Frameworks/AntDesign/src/transport/dashboardTransport.ts b/Apps/Frameworks/AntDesign/src/transport/dashboardTransport.ts new file mode 100644 index 0000000..bc5c28a --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/transport/dashboardTransport.ts @@ -0,0 +1,127 @@ +import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; +import { parseDashboardBootstrap } from '../schema/dashboard'; +import type { DashboardBootstrap, EndpointDescriptor } from '../types/dashboard'; +import { useRuntimeStore } from '../state/runtimeStore'; + +type ConnectionInput = { + baseUrl: string; + dashboardId: string; + pageId: string; + sessionId: string; + timezone: string; +}; + +type EndpointOptions = { + query?: Record; +}; + +function buildUrl(baseUrl: string, path: string): string { + return `${baseUrl}${path}`; +} + +function createRequestBody(body: unknown, contentType?: string): BodyInit | undefined { + if (typeof body === 'undefined' || body === null) { + return undefined; + } + + if (body instanceof FormData) { + return body; + } + + if (typeof body === 'string') { + return body; + } + + if (contentType === 'text/plain') { + return String(body); + } + + return JSON.stringify(body); +} + +export async function fetchDashboardBootstrap(baseUrl: string): Promise { + const response = await fetch(buildUrl(baseUrl, '/api/internal/dashboard'), { + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Dashboard bootstrap failed with status ${response.status}.`); + } + + const payload: unknown = await response.json(); + return parseDashboardBootstrap(payload); +} + +export function createDashboardHubConnection({ + baseUrl, + dashboardId, + pageId, + sessionId, + timezone, +}: ConnectionInput): HubConnection { + const query = new URLSearchParams({ + dashboardid: dashboardId, + pageid: pageId, + sessionid: sessionId, + timezone, + }); + + return new HubConnectionBuilder() + .withUrl(buildUrl(baseUrl, `/dashboardhub?${query.toString()}`), { + withCredentials: true, + }) + .withAutomaticReconnect() + .configureLogging(LogLevel.Warning) + .build(); +} + +export async function invokeComponentEndpoint( + baseUrl: string, + endpoint: EndpointDescriptor, + body?: unknown, + options?: EndpointOptions, +): Promise { + const query = new URLSearchParams(options?.query ?? {}); + const querySuffix = query.size > 0 ? `?${query.toString()}` : ''; + const contentType = body instanceof FormData ? undefined : endpoint.contentType; + const connectionId = useRuntimeStore.getState().connectionId; + const requestBody = createRequestBody(body, contentType); + const requestInit: RequestInit = { + method: 'POST', + credentials: 'include', + headers: { + Accept: endpoint.accept ?? 'application/json', + ...(connectionId ? { UDConnectionId: connectionId } : {}), + ...(contentType ? { 'Content-Type': contentType } : {}), + }, + }; + + if (requestBody !== undefined) { + requestInit.body = requestBody; + } + + return fetch(buildUrl(baseUrl, `/api/internal/component/element/${endpoint.name}${querySuffix}`), requestInit); +} + +export async function getComponentEndpoint(baseUrl: string, endpointId: string): Promise { + return fetch(buildUrl(baseUrl, `/api/internal/component/element/${endpointId}`), { + method: 'GET', + credentials: 'include', + }); +} + +export function startDashboardDownload(baseUrl: string, dashboardId: string, downloadId: string, fileName?: string) { + const anchor = document.createElement('a'); + anchor.href = buildUrl(baseUrl, `/api/internal/dashboard/download/${dashboardId}/${downloadId}`); + + if (fileName) { + anchor.download = fileName; + } + + document.body.append(anchor); + anchor.click(); + anchor.remove(); +} diff --git a/Apps/Frameworks/AntDesign/src/types/dashboard.ts b/Apps/Frameworks/AntDesign/src/types/dashboard.ts new file mode 100644 index 0000000..ea31071 --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/types/dashboard.ts @@ -0,0 +1,34 @@ +export type DashboardPrimitive = string | number | boolean | null; + +export type DescriptorContent = DashboardPrimitive | DashboardDescriptor | DescriptorContent[]; + +export type EndpointDescriptor = { + endpoint?: boolean | undefined; + name: string; + javaScript?: string | undefined; + websocket?: boolean | undefined; + accept?: string | undefined; + contentType?: string | undefined; + [key: string]: unknown; +}; + +export type DashboardDescriptor = { + type: string; + id?: string | undefined; + content?: DescriptorContent | undefined; + isPlugin?: boolean | undefined; + assetId?: string | undefined; + [key: string]: unknown; +}; + +export type DashboardBootstrap = { + dashboard: DescriptorContent; + sessionId: string; + pageId: string; + authType?: string | undefined; + roles?: string[] | undefined; + user?: string | undefined; + idleTimeout?: number | undefined; + dashboardName?: string | undefined; + developerLicense?: boolean | undefined; +}; diff --git a/Apps/Frameworks/AntDesign/src/types/universal-dashboard.d.ts b/Apps/Frameworks/AntDesign/src/types/universal-dashboard.d.ts new file mode 100644 index 0000000..81ab61f --- /dev/null +++ b/Apps/Frameworks/AntDesign/src/types/universal-dashboard.d.ts @@ -0,0 +1,7 @@ +declare module 'universal-dashboard' { + import type { ComponentType } from 'react'; + + export function withComponentFeatures>( + component: ComponentType, + ): ComponentType; +} \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/button.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/button.spec.ts new file mode 100644 index 0000000..d318b41 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/button.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from './harnessFixture'; + +test('renders the button component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Button' }).click(); + + await expect(page).toHaveURL(/#\/components\/button/); + await expect(page.getByRole('heading', { name: 'Button' })).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-button descriptor that maps the PowerShell command surface to the core Ant Design Button TypeScript definition used by the client runtime. The command mirrors the Ant Design Button API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs.', + ), + ).toBeVisible(); + await expect(page.getByText('When To Use', { exact: true })).toBeVisible(); + await expect(page.getByText('A button represents an operation or a short sequence of operations.')).toBeVisible(); + await expect(page.getByText('Use a primary button for the main action in a section, and keep it to one primary action when possible.')).toBeVisible(); + await expect(page.getByText('Syntactic sugar', { exact: true })).toBeVisible(); + await expect(page.getByText('Color and variant', { exact: true })).toBeVisible(); + await expect(page.getByText("New-UDAntDesignButton -Text 'Primary Button' -Type primary")).toBeVisible(); + await expect(page.getByText("New-UDAntDesignButton -Text 'Primary Solid' -Color primary -Variant solid")).toBeVisible(); +}); + +test('shows live previews for the documented button examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Button' }).click(); + + await expect(page.getByRole('button', { name: 'Primary Button' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Search End' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete Record' })).toHaveClass(/ant-btn-dangerous/); + await expect(page.getByRole('button', { name: 'Full Width Primary' })).toHaveClass(/ant-btn-block/); + await expect(page.getByRole('link', { name: 'Link Button' })).toHaveAttribute( + 'href', + 'https://ant.design/components/button/', + ); + await expect(page.getByRole('button', { name: 'Saving Changes' }).locator('.ant-btn-loading-icon')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Ghost Primary' })).toHaveClass(/ant-btn-background-ghost/); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/checkbox.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/checkbox.spec.ts new file mode 100644 index 0000000..11fde31 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/checkbox.spec.ts @@ -0,0 +1,58 @@ +import { expect, test } from './harnessFixture'; + +test('renders the checkbox component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Checkbox' }).click(); + + await expect(page).toHaveURL(/#\/components\/checkbox/); + await expect(page.getByRole('heading', { name: 'Checkbox' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-checkbox descriptor that maps the PowerShell command surface to the Ant Design Checkbox component used by the client runtime. The command mirrors the core Checkbox API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks.', + ), + ).toBeVisible(); + + const whenToUseCard = page.locator('.docs-section-card').filter({ hasText: 'When To Use' }); + await expect( + whenToUseCard.getByText('Used for selecting multiple values from several options.'), + ).toBeVisible(); + await expect( + whenToUseCard.getByText( + 'If you use only one checkbox, it is the same as using Switch to toggle between two states. The difference is that Switch will trigger the state change directly, but Checkbox just marks the state as changed and this needs to be submitted.', + ), + ).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Basic' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Disabled' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Indeterminate' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Custom styling' })).toBeVisible(); + await expect(page.getByText("New-UDAntDesignCheckbox -Label 'Partially selected permissions' -Indeterminate")).toBeVisible(); +}); + +test('shows live previews for the documented checkbox examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Checkbox' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic' }); + await expect(basicCard.locator('.ant-checkbox').first()).toBeVisible(); + await expect(basicCard.locator('.ant-checkbox').nth(1)).toHaveClass(/ant-checkbox-checked/); + await expect(basicCard.locator('.ant-checkbox-label').filter({ hasText: 'Remember me' })).toBeVisible(); + await expect(basicCard.locator('.ant-checkbox-label').filter({ hasText: 'Send status updates' })).toBeVisible(); + + const disabledCard = page.locator('.docs-section-card').filter({ hasText: 'Disabled' }); + await expect(disabledCard.locator('.ant-checkbox-wrapper').first()).toHaveClass(/ant-checkbox-wrapper-disabled/); + await expect(disabledCard.locator('.ant-checkbox').nth(1)).toHaveClass(/ant-checkbox-checked/); + + const indeterminateCard = page.locator('.docs-section-card').filter({ hasText: 'Indeterminate' }); + await expect(indeterminateCard.locator('.ant-checkbox')).toHaveClass(/ant-checkbox-indeterminate/); + await expect( + indeterminateCard.locator('.ant-checkbox-label').filter({ hasText: 'Partially selected permissions' }), + ).toBeVisible(); + + const styledCard = page.locator('.docs-section-card').filter({ hasText: 'Custom styling' }); + await expect(styledCard.locator('.ant-checkbox-wrapper').filter({ hasText: 'Styled option' })).toHaveCSS( + 'color', + 'rgb(212, 107, 8)', + ); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/grid.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/grid.spec.ts new file mode 100644 index 0000000..ee7f6d6 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/grid.spec.ts @@ -0,0 +1,65 @@ +import { expect, test } from './harnessFixture'; + +test('renders the grid component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Grid' }).click(); + + await expect(page).toHaveURL(/#\/components\/grid/); + await expect(page.getByRole('heading', { name: 'Grid' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-row descriptor that maps the PowerShell command surface to the Ant Design Row component used by the client runtime. Use grid rows as the outer layout container for Ant Design grid columns so dashboard content can be arranged in the standard 24-column grid system.', + ), + ).toBeVisible(); + await expect(page.getByText('When To Use', { exact: true })).toBeVisible(); + await expect( + page.getByText('Use rows to define horizontal layout bands and place only Ant Design grid columns directly inside the row content.'), + ).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Basic grid' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Responsive gutter' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Justify and align' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Flex fill' })).toBeVisible(); + await expect(page.getByText('New-UDAntDesignCol -Span 12 -Content (New-UDAntDesignText -Text \'col-12\')')).toBeVisible(); +}); + +test('shows live previews for the documented grid examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Grid' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic grid' }); + await expect(basicCard.locator('.ant-row')).toBeVisible(); + await expect(basicCard.locator('.ant-col-12')).toHaveCount(2); + await expect(basicCard.locator('.ant-row .ant-col-12 .ant-typography').filter({ hasText: 'col-12' })).toHaveCount(2); + + const responsiveCard = page.locator('.docs-section-card').filter({ hasText: 'Responsive gutter' }); + await expect(responsiveCard.locator('.ant-row')).toBeVisible(); + await expect( + responsiveCard.locator('.ant-row .ant-typography').filter({ hasText: 'Responsive gutter' }), + ).toBeVisible(); + await expect( + responsiveCard.locator('.ant-row .ant-typography').filter({ hasText: 'Horizontal and vertical spacing' }), + ).toBeVisible(); + + const justifyCard = page.locator('.docs-section-card').filter({ hasText: 'Justify and align' }); + await expect(justifyCard.locator('.ant-row-space-between')).toBeVisible(); + await expect(justifyCard.locator('.ant-row-middle')).toBeVisible(); + await expect(justifyCard.locator('.ant-row .ant-typography').filter({ hasText: 'Left' })).toBeVisible(); + await expect(justifyCard.locator('.ant-row .ant-typography').filter({ hasText: 'Center' })).toBeVisible(); + await expect(justifyCard.locator('.ant-row .ant-typography').filter({ hasText: 'Right' })).toBeVisible(); + + const orderCard = page.locator('.docs-section-card').filter({ hasText: 'Offset and order' }); + await expect(orderCard.locator('.ant-col-offset-6')).toBeVisible(); + await expect(orderCard.locator('.ant-row .ant-typography').filter({ hasText: 'First with offset' })).toBeVisible(); + await expect( + orderCard.locator('.ant-row .ant-typography').filter({ hasText: 'Second in visual order' }), + ).toBeVisible(); + + const flexCard = page.locator('.docs-section-card').filter({ hasText: 'Flex fill' }); + await expect(flexCard.locator('.ant-row .ant-typography').filter({ hasText: '100px' })).toBeVisible(); + await expect(flexCard.locator('.ant-row .ant-typography').filter({ hasText: 'Auto width' })).toBeVisible(); + await expect( + flexCard.locator('.ant-row .ant-typography').filter({ hasText: 'Flexible remainder' }), + ).toBeVisible(); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/harness.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/harness.spec.ts new file mode 100644 index 0000000..82715c1 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/harness.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from './harnessFixture'; + +test('loads the Ant Design component docs shell', async ({ page, harness }) => { + await harness.gotoShell(page); + + await expect(page.getByRole('heading', { name: 'Ant Design Components' }).first()).toBeVisible(); + await expect(page.getByText('Generated from comment-based help')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Button' })).toBeVisible(); + await expect(page.getByText(/Connection state:/)).toContainText('connected'); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/harnessFixture.ts b/Apps/Frameworks/AntDesign/testing/playwright/harnessFixture.ts new file mode 100644 index 0000000..0491a20 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/harnessFixture.ts @@ -0,0 +1,66 @@ +import { test as base, expect, type APIRequestContext, type Page, type APIResponse } from '@playwright/test'; + +type HarnessMessageOptions = { + connectionId?: string; + dashboardId?: string; +}; + +type HarnessDownloadRegistration = { + fileName: string; + content: string; + contentType?: string; +}; + +type HarnessController = { + baseUrl: string; + gotoShell: (page: Page) => Promise; + sendMessage: (messageType: string, data?: unknown, options?: HarnessMessageOptions) => Promise; + registerDownload: (id: string, download: HarnessDownloadRegistration) => Promise; +}; + +async function assertOk(requestPromise: Promise) { + const response = await requestPromise; + expect(response.ok()).toBeTruthy(); + return response; +} + +function createHarnessController(request: APIRequestContext, baseUrl: string): HarnessController { + return { + baseUrl, + async gotoShell(page: Page) { + await page.goto(baseUrl, { waitUntil: 'networkidle' }); + }, + async sendMessage(messageType: string, data?: unknown, options?: HarnessMessageOptions) { + await assertOk( + request.post(`${baseUrl}/api/harness/messages`, { + data: { + messageType, + data, + connectionId: options?.connectionId, + dashboardId: options?.dashboardId, + }, + }), + ); + }, + async registerDownload(id: string, download: HarnessDownloadRegistration) { + await assertOk( + request.post(`${baseUrl}/api/harness/downloads/${id}`, { + data: { + fileName: download.fileName, + content: download.content, + contentType: download.contentType ?? 'text/plain', + }, + }), + ); + }, + }; +} + +export const test = base.extend<{ harness: HarnessController }>({ + harness: async ({ request, baseURL }, use) => { + const resolvedBaseUrl = baseURL ?? 'http://127.0.0.1:5057'; + await use(createHarnessController(request, resolvedBaseUrl)); + }, +}); + +export { expect }; \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/input.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/input.spec.ts new file mode 100644 index 0000000..bff7aae --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/input.spec.ts @@ -0,0 +1,63 @@ +import { expect, test } from './harnessFixture'; + +test('renders the input component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Input' }).click(); + + await expect(page).toHaveURL(/#\/components\/input/); + await expect(page.getByRole('heading', { name: 'Input' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-input descriptor that maps the PowerShell command surface to the Ant Design Input family used by the client runtime.', + ), + ).toBeVisible(); + + const whenToUseCard = page.locator('.docs-section-card').filter({ hasText: 'When To Use' }); + await expect(whenToUseCard.getByText('A user input in a form field is needed.')).toBeVisible(); + await expect(whenToUseCard.getByText('A search input is required.')).toBeVisible(); + + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: /^Basic usage$/ })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: /^Search box$/ })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: /^OTP$/ })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: /^Custom count logic$/ })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: /^Custom semantic dom styling$/ })).toBeVisible(); + await expect(page.getByText("New-UDAntDesignInput -Mode search -Placeholder 'input search text' -EnterButton $true")).toBeVisible(); +}); + +test('shows live previews for the documented input examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Input' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic usage' }); + await expect(basicCard.getByPlaceholder('Basic usage')).toBeVisible(); + + const sizeCard = page.locator('.docs-section-card').filter({ hasText: 'Three sizes of Input' }); + await expect(sizeCard.getByPlaceholder('large size')).toBeVisible(); + await expect(sizeCard.getByPlaceholder('default size')).toBeVisible(); + await expect(sizeCard.getByPlaceholder('small size')).toBeVisible(); + + const searchCard = page.locator('.docs-section-card').filter({ hasText: 'Search box' }); + await expect(searchCard.getByPlaceholder('input search text').first()).toBeVisible(); + await expect(searchCard.getByRole('button', { name: 'Search' }).first()).toBeVisible(); + + const textareaCard = page.locator('.docs-section-card').filter({ hasText: 'TextArea' }); + await expect(textareaCard.locator('textarea').first()).toBeVisible(); + await expect(textareaCard.getByPlaceholder('maxLength is 6')).toBeVisible(); + + const otpCard = page.locator('.docs-section-card').filter({ hasText: 'OTP' }); + await expect(otpCard.getByRole('heading', { name: 'With formatter (Upcase)' })).toBeVisible(); + await expect(otpCard.locator('input').nth(5)).toBeVisible(); + + const passwordCard = page.locator('.docs-section-card').filter({ hasText: 'Password box' }); + await expect(passwordCard.getByPlaceholder('input password').first()).toBeVisible(); + await expect(passwordCard.getByPlaceholder('disabled input password')).toBeVisible(); + + const countCard = page.locator('.docs-section-card').filter({ hasText: 'With character counting' }); + await expect(countCard.locator('.ant-input-show-count-suffix').first()).toBeVisible(); + + const statusCard = page.locator('.docs-section-card').filter({ hasText: 'Status' }); + await expect(statusCard.getByPlaceholder('Error with prefix')).toBeVisible(); + await expect(statusCard.getByPlaceholder('Warning with prefix')).toBeVisible(); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/layout.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/layout.spec.ts new file mode 100644 index 0000000..266c77c --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/layout.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from './harnessFixture'; + +test('renders the layout component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Layout' }).click(); + + await expect(page).toHaveURL(/#\/components\/layout/); + await expect(page.getByRole('heading', { name: 'Layout' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-layout descriptor that maps the PowerShell command surface to the Ant Design Layout component used by the client runtime. Use the layout wrapper to compose page chrome with Header, Sider, Content, and Footer regions while keeping the descriptor contract aligned with the upstream Ant Design layout model.', + ), + ).toBeVisible(); + await expect(page.getByText('When To Use', { exact: true })).toBeVisible(); + await expect( + page.getByText('Layout is the outer container for page structure and can contain nested layouts when a page needs both top and side navigation.'), + ).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Basic structure' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Header and sider' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Collapsible sider' })).toBeVisible(); + await expect(page.locator('.docs-section-card .ant-card-head-title').filter({ hasText: 'Responsive sider' })).toBeVisible(); + await expect(page.getByText("New-UDAntDesignLayoutSider -Breakpoint lg -CollapsedWidth 0 -Width 220")).toBeVisible(); +}); + +test('shows live previews for the documented layout examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Layout' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic structure' }); + await expect(basicCard.locator('.ant-layout')).toBeVisible(); + await expect(basicCard.locator('.ant-layout-header')).toBeVisible(); + await expect(basicCard.locator('.ant-layout-content')).toBeVisible(); + await expect(basicCard.locator('.ant-layout-footer')).toBeVisible(); + await expect(basicCard.locator('.ant-layout-header')).toContainText('Header'); + await expect(basicCard.locator('.ant-layout-content')).toContainText('Content'); + await expect(basicCard.locator('.ant-layout-footer')).toContainText('Footer'); + + const headerSiderCard = page.locator('.docs-section-card').filter({ hasText: 'Header and sider' }); + await expect(headerSiderCard.locator('.ant-layout-has-sider')).toBeVisible(); + await expect(headerSiderCard.locator('.ant-layout-sider')).toBeVisible(); + await expect(headerSiderCard.locator('.ant-layout-sider')).toContainText('Sider'); + await expect(headerSiderCard.locator('.ant-layout-content')).toContainText('Content'); + + const collapsibleCard = page.locator('.docs-section-card').filter({ hasText: 'Collapsible sider' }); + await expect(collapsibleCard.locator('.ant-layout-sider')).toBeVisible(); + await expect(collapsibleCard.locator('.ant-layout-sider-collapsed')).toBeVisible(); + await expect(collapsibleCard.locator('.ant-layout-content')).toContainText('Content with collapsible navigation'); + + const responsiveCard = page.locator('.docs-section-card').filter({ hasText: 'Responsive sider' }); + await expect(responsiveCard.locator('.ant-layout-sider')).toBeVisible(); + await expect(responsiveCard.locator('.ant-layout-sider')).toContainText('Responsive sider'); + await expect(responsiveCard.locator('.ant-layout-content')).toContainText('Resize the page to let the sider collapse at the large breakpoint.'); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/message.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/message.spec.ts new file mode 100644 index 0000000..00594c6 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/message.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from './harnessFixture'; + +test('renders a server-pushed Ant Design message', async ({ page, harness }) => { + await harness.gotoShell(page); + + await expect(page.getByText(/Connection state:/)).toContainText('connected'); + + await harness.sendMessage('antdesign-message', { + content: 'Ant Design message transport is working.', + type: 'success', + duration: 0, + key: 'transport-check', + }); + + await expect(page.getByText('Ant Design message transport is working.')).toBeVisible(); +}); + +test('renders the Message documentation examples as clickable buttons', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Message' }).click(); + + await expect(page.getByRole('button', { name: 'Save Changes' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Start Sync' })).toBeVisible(); + + await page.getByRole('button', { name: 'Save Changes' }).click(); + await expect(page.getByText('Saved changes.')).toBeVisible(); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/rate.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/rate.spec.ts new file mode 100644 index 0000000..12295ac --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/rate.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from './harnessFixture'; + +test('renders the rate component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Rate' }).click(); + + await expect(page).toHaveURL(/#\/components\/rate/); + await expect(page.getByRole('heading', { name: 'Rate' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-rate descriptor that maps the PowerShell command surface to the Ant Design Rate component used by the client runtime. The command mirrors the core Rate API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks.', + ), + ).toBeVisible(); + + const whenToUseCard = page.locator('.docs-section-card').filter({ hasText: 'When To Use' }); + await expect(whenToUseCard.getByText('Show evaluation.')).toBeVisible(); + await expect(whenToUseCard.getByText('A quick rating operation on something.')).toBeVisible(); + await expect(page.getByText('Half star', { exact: true })).toBeVisible(); + await expect(page.getByText('Clear star', { exact: true })).toBeVisible(); + await expect(page.getByText('Other character', { exact: true })).toBeVisible(); + await expect(page.getByText('New-UDAntDesignRate -AllowHalf:$true -DefaultValue 2.5')).toBeVisible(); +}); + +test('shows live previews for the documented rate examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Rate' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic' }); + await expect(basicCard.locator('.ant-rate')).toBeVisible(); + await expect(basicCard.locator('.ant-rate-star')).toHaveCount(5); + + const sizesCard = page.locator('.docs-section-card').filter({ hasText: 'Sizes' }); + await expect(sizesCard.locator('.ant-rate').first()).toHaveCSS('font-size', '15px'); + await expect(sizesCard.locator('.ant-rate').nth(1)).toHaveCSS('font-size', '20px'); + await expect(sizesCard.locator('.ant-rate').nth(2)).toHaveCSS('font-size', '25px'); + + const halfStarCard = page.locator('.docs-section-card').filter({ hasText: 'Half star' }); + await expect(halfStarCard.locator('.ant-rate-star-half').first()).toBeVisible(); + + const readOnlyCard = page.locator('.docs-section-card').filter({ hasText: 'Read only' }); + await expect(readOnlyCard.locator('.ant-rate')).toHaveClass(/ant-rate-disabled/); + + const otherCharacterCard = page.locator('.docs-section-card').filter({ hasText: 'Other character' }); + await expect(otherCharacterCard.locator('.anticon-heart').first()).toBeVisible(); + await expect(otherCharacterCard.getByText('A').first()).toBeVisible(); + await expect(otherCharacterCard.getByText('好').first()).toBeVisible(); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/switch.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/switch.spec.ts new file mode 100644 index 0000000..f1e7413 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/switch.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from './harnessFixture'; + +test('renders the switch component documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Switch' }).click(); + + await expect(page).toHaveURL(/#\/components\/switch/); + await expect(page.getByRole('heading', { name: 'Switch' }).first()).toBeVisible(); + await expect( + page.getByText( + 'Creates an antd-switch descriptor that maps the PowerShell command surface to the Ant Design Switch component used by the client runtime. The command mirrors the core Switch API closely so the documented PowerShell examples can follow the same usage patterns as the upstream docs while still supporting PowerShell Universal endpoint callbacks.', + ), + ).toBeVisible(); + + const whenToUseCard = page.locator('.docs-section-card').filter({ hasText: 'When To Use' }); + await expect( + whenToUseCard.getByText('If you need to represent the switching between two states or on-off state.'), + ).toBeVisible(); + await expect( + whenToUseCard.getByText( + 'The difference between Switch and Checkbox is that Switch will trigger a state change directly when you toggle it, while Checkbox is generally used for state marking, which should work in conjunction with submit operation.', + ), + ).toBeVisible(); + await expect(page.getByText('Basic', { exact: true })).toBeVisible(); + await expect(page.getByText('Text & icon', { exact: true })).toBeVisible(); + await expect(page.getByText('Custom semantic dom styling', { exact: true })).toBeVisible(); + await expect(page.getByText('New-UDAntDesignSwitch -DefaultChecked $true -Size small')).toBeVisible(); +}); + +test('shows live previews for the documented switch examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Switch' }).click(); + + const basicCard = page.locator('.docs-section-card').filter({ hasText: 'Basic' }); + const basicPreviewSurface = basicCard.locator('.docs-preview-surface').first(); + await expect(basicCard.locator('.ant-switch').first()).toBeVisible(); + await expect(basicCard.locator('.ant-switch').nth(1)).toHaveClass(/ant-switch-checked/); + + const basicPreviewSurfaceBox = await basicPreviewSurface.boundingBox(); + const basicSwitchBox = await basicCard.locator('.ant-switch').first().boundingBox(); + + expect(basicPreviewSurfaceBox).not.toBeNull(); + expect(basicSwitchBox).not.toBeNull(); + expect(basicSwitchBox!.width).toBeLessThan(basicPreviewSurfaceBox!.width); + + const disabledCard = page.locator('.docs-section-card').filter({ hasText: 'Disabled' }); + await expect(disabledCard.locator('.ant-switch').first()).toHaveClass(/ant-switch-disabled/); + + const textIconCard = page.locator('.docs-section-card').filter({ hasText: 'Text & icon' }); + await expect(textIconCard.locator('.ant-switch-inner').first()).toContainText('0'); + await expect(textIconCard.locator('.anticon-check').first()).toBeVisible(); + + const sizeCard = page.locator('.docs-section-card').filter({ hasText: 'Two sizes' }); + await expect(sizeCard.locator('.ant-switch').nth(1)).toHaveClass(/ant-switch-small/); + + const loadingCard = page.locator('.docs-section-card').filter({ hasText: 'Loading' }); + await expect(loadingCard.locator('.ant-switch-loading-icon').first()).toBeVisible(); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/testing/playwright/typography.spec.ts b/Apps/Frameworks/AntDesign/testing/playwright/typography.spec.ts new file mode 100644 index 0000000..9cd3405 --- /dev/null +++ b/Apps/Frameworks/AntDesign/testing/playwright/typography.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from './harnessFixture'; + +test('renders the typography documentation from comment-based help', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Typography' }).click(); + + await expect(page).toHaveURL(/#\/components\/typography/); + await expect(page.getByRole('heading', { name: 'Typography' }).first()).toBeVisible(); + const whenToUseCard = page.locator('.docs-section-card').filter({ hasText: 'When To Use' }); + await expect( + whenToUseCard.getByText('Basic text writing, including headings, body text, lists, and more.').first(), + ).toBeVisible(); + await expect(whenToUseCard.getByText('When you need copyable, editable, or ellipsis text treatments.')).toBeVisible(); + await expect( + page.getByText("New-UDAntDesignTypography -Kind title -Level 1 -Text 'h1. Ant Design'"), + ).toBeVisible(); + await expect( + page.getByText("New-UDAntDesignTypography -Text 'This is a copyable text.' -Copyable"), + ).toBeVisible(); +}); + +test('shows live previews for the documented typography examples', async ({ page, harness }) => { + await harness.gotoShell(page); + + await page.getByRole('menuitem', { name: 'Typography' }).click(); + + const titleCard = page.locator('.docs-section-card').filter({ hasText: 'Title Component' }); + const textAndLinkCard = page.locator('.docs-section-card').filter({ hasText: 'Text and Link Component' }); + const copyableCard = page.locator('.docs-section-card').filter({ hasText: 'Copyable' }); + + await expect(titleCard.getByRole('heading', { name: 'h1. Ant Design' }).first()).toBeVisible(); + await expect(textAndLinkCard.locator('span.ant-typography').filter({ hasText: 'Ant Design (secondary)' }).first()).toHaveClass( + /ant-typography-secondary/, + ); + await expect(textAndLinkCard.getByRole('link', { name: 'Ant Design (Link)' })).toHaveAttribute('href', 'https://ant.design/'); + await expect(copyableCard.locator('.docs-preview-surface .ant-typography-copy').first()).toBeVisible(); + + const ellipsisCard = page.locator('.docs-section-card').filter({ hasText: 'Ellipsis' }); + const ellipsisSurface = ellipsisCard.locator('.docs-preview-surface').first(); + await ellipsisSurface.getByText('Expand').click(); + await expect(ellipsisSurface).toContainText( + 'Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.', + ); +}); \ No newline at end of file diff --git a/Apps/Frameworks/AntDesign/tsconfig.app.json b/Apps/Frameworks/AntDesign/tsconfig.app.json new file mode 100644 index 0000000..9af2750 --- /dev/null +++ b/Apps/Frameworks/AntDesign/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/Apps/Frameworks/AntDesign/tsconfig.json b/Apps/Frameworks/AntDesign/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/Apps/Frameworks/AntDesign/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/Apps/Frameworks/AntDesign/tsconfig.node.json b/Apps/Frameworks/AntDesign/tsconfig.node.json new file mode 100644 index 0000000..34bc3cd --- /dev/null +++ b/Apps/Frameworks/AntDesign/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "types": ["node"] + }, + "include": ["vite.config.ts", "eslint.config.js"] +} diff --git a/Apps/Frameworks/AntDesign/vite.config.d.ts b/Apps/Frameworks/AntDesign/vite.config.d.ts new file mode 100644 index 0000000..340562a --- /dev/null +++ b/Apps/Frameworks/AntDesign/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/Apps/Frameworks/AntDesign/vite.config.js b/Apps/Frameworks/AntDesign/vite.config.js new file mode 100644 index 0000000..b05fc94 --- /dev/null +++ b/Apps/Frameworks/AntDesign/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + manifest: 'manifest.json', + sourcemap: true, + cssCodeSplit: true, + rollupOptions: { + output: { + entryFileNames: 'assets/[name]-[hash].js', + chunkFileNames: 'assets/chunks/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, + }, +}); diff --git a/Apps/Frameworks/AntDesign/vite.config.ts b/Apps/Frameworks/AntDesign/vite.config.ts new file mode 100644 index 0000000..798a337 --- /dev/null +++ b/Apps/Frameworks/AntDesign/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + manifest: 'manifest.json', + sourcemap: true, + cssCodeSplit: true, + rollupOptions: { + output: { + entryFileNames: 'assets/[name]-[hash].js', + chunkFileNames: 'assets/chunks/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, + }, +}); diff --git a/Apps/Frameworks/Harness/.gitignore b/Apps/Frameworks/Harness/.gitignore new file mode 100644 index 0000000..5967294 --- /dev/null +++ b/Apps/Frameworks/Harness/.gitignore @@ -0,0 +1,2 @@ +**/bin/ +**/obj/ diff --git a/Apps/Frameworks/Harness/README.md b/Apps/Frameworks/Harness/README.md new file mode 100644 index 0000000..4fea69f --- /dev/null +++ b/Apps/Frameworks/Harness/README.md @@ -0,0 +1,138 @@ +# PSU Framework Harness + +This folder contains a lightweight ASP.NET Core host that mimics the PowerShell Universal dashboard framework contract without requiring the full PSU runtime. + +The harness focuses on the framework-facing surface: + +- serving framework static assets from published-folder style request paths +- returning dashboard bootstrap data from PowerShell scripts +- executing PowerShell endpoint scripts for HTTP and websocket events +- sending and receiving SignalR messages on `/dashboardhub` +- storing session-state fallbacks and temporary downloads + +## What it hosts + +The harness exposes the same compatibility surface a custom framework cares about: + +- `GET /api/internal/dashboard` +- `GET /api/internal/component/element/{id}` +- `POST /api/internal/component/element/{id}` +- `POST /api/internal/component/element/sessionState/{requestId}` +- `GET /api/internal/dashboard/download/{dashboardId}/{id}` +- SignalR hub at `/dashboardhub` + +It also exposes small test-oriented admin endpoints: + +- `GET /api/harness/connections` +- `POST /api/harness/messages` +- `POST /api/harness/downloads/{id}` + +These make the harness useful for Playwright coverage without needing a full PSU instance. + +## Run + +From this folder: + +```powershell +dotnet run --project .\src\PowerShellUniversal.Frameworks.Harness\PowerShellUniversal.Frameworks.Harness.csproj --urls http://localhost:5057 +``` + +Then open `http://localhost:5057`. + +The default sample definition mounts the Ant Design framework bundle from [Apps/Frameworks/AntDesign/dist](d:/git/powershell-universal-gallery/Apps/Frameworks/AntDesign/dist), resolves the current `manifest.json`, and loads a small demo dashboard without hardcoding hashed asset names. + +## Definition file + +The harness reads a PowerShell definition script from `Harness:DefinitionPath` in [Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/appsettings.json](d:/git/powershell-universal-gallery/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/appsettings.json). + +The default definition is [Apps/Frameworks/Harness/sample/harness.ps1](d:/git/powershell-universal-gallery/Apps/Frameworks/Harness/sample/harness.ps1). + +The definition script returns a hashtable with these keys: + +- `DashboardScript`: path to a PowerShell script that returns either the full bootstrap object or just the root dashboard descriptor +- `EndpointRoot`: folder containing endpoint scripts named `{endpointId}.ps1` +- `Endpoints`: optional explicit endpoint-id to script-path map +- `StaticAssets`: request-path mappings for framework bundles +- `Shell`: simple host page settings such as title, mount id, scripts, and styles + +Example: + +```powershell +$antDesignDistPath = Join-Path $PSScriptRoot '..\..\AntDesign\dist' +$antDesignAssetBasePath = '/frameworks/ant-design' +$antDesignEntryPoint = Get-AntDesignHarnessEntryPoint -DistPath $antDesignDistPath -BasePath $antDesignAssetBasePath + +@{ + DashboardScript = Join-Path $PSScriptRoot 'dashboard.ps1' + EndpointRoot = Join-Path $PSScriptRoot 'endpoints' + StaticAssets = @( + @{ + RequestPath = $antDesignAssetBasePath + Path = $antDesignDistPath + } + ) + Shell = @{ + Title = 'PSU Framework Harness' + MountId = 'root' + Scripts = $antDesignEntryPoint.Scripts + Styles = $antDesignEntryPoint.Styles + } +} +``` + +## PowerShell script contract + +Each script executes with two ambient variables: + +- `$HarnessContext`: request or websocket context information +- `$PsuHarness`: helper API instance + +The harness also exposes script-location helpers for relative path resolution: + +- `$HarnessScriptRoot` +- `$HarnessScriptPath` + +Helper functions are also available: + +- `Send-PSUHarnessMessage` +- `Set-PSUHarnessDownload` +- `Set-PSUHarnessSessionState` +- `Get-PSUHarnessConnections` + +`$HarnessContext` includes fields such as: + +- `DashboardId` +- `SessionId` +- `PageId` +- `ConnectionId` +- `EndpointId` +- `Method` +- `EventName` +- `EventData` +- `Location` +- `Query` +- `Headers` +- `Cookies` +- `Form` +- `Body` +- `JsonBody` +- `Files` + +Endpoint scripts can return hashtables, arrays, strings, and other serializable values. The harness normalizes the output to JSON. + +## Playwright usage + +The harness is meant to be the default browser host for framework tests. + +Typical flow: + +1. Start the harness. +2. Navigate Playwright to `/`. +3. Use `POST /api/harness/messages` to push `setState`, `addElement`, `download`, or other websocket messages. +4. Use `POST /api/harness/downloads/{id}` before sending a `download` message when download flows need to be exercised. + +## Notes + +- Static asset mappings are loaded at startup. If you change `StaticAssets`, restart the harness. +- Dashboard and endpoint scripts are reloaded when the definition file changes. +- Authentication, authorization, and PSU-specific variable scoping are intentionally out of scope here. diff --git a/Apps/Frameworks/Harness/sample/dashboard.ps1 b/Apps/Frameworks/Harness/sample/dashboard.ps1 new file mode 100644 index 0000000..728ffde --- /dev/null +++ b/Apps/Frameworks/Harness/sample/dashboard.ps1 @@ -0,0 +1,35 @@ +$message = 'Harness ready' + +if ($null -ne $HarnessContext -and $HarnessContext.Query.ContainsKey('message')) { + $message = [string]$HarnessContext.Query['message'] +} + +@{ + dashboard = @{ + type = 'antd-text' + id = 'root-text' + text = $message + content = @( + @{ + type = 'antd-button' + id = 'http-button' + text = 'Invoke HTTP endpoint' + onClick = @{ + endpoint = $true + name = 'demo-button' + } + }, + @{ + type = 'antd-button' + id = 'server-push-button' + text = 'Invoke server push flow' + onClick = @{ + endpoint = $true + name = 'demo-websocket' + } + } + ) + } + dashboardName = 'Framework Harness Demo' + developerLicense = $true +} diff --git a/Apps/Frameworks/Harness/sample/endpoints/demo-button.ps1 b/Apps/Frameworks/Harness/sample/endpoints/demo-button.ps1 new file mode 100644 index 0000000..35a0b37 --- /dev/null +++ b/Apps/Frameworks/Harness/sample/endpoints/demo-button.ps1 @@ -0,0 +1,12 @@ +$payload = $HarnessContext.JsonBody + +if ($null -eq $payload) { + $payload = $HarnessContext.Body +} + +@{ + ok = $true + endpoint = $HarnessContext.EndpointId + method = $HarnessContext.Method + received = $payload +} diff --git a/Apps/Frameworks/Harness/sample/endpoints/demo-websocket.ps1 b/Apps/Frameworks/Harness/sample/endpoints/demo-websocket.ps1 new file mode 100644 index 0000000..7653080 --- /dev/null +++ b/Apps/Frameworks/Harness/sample/endpoints/demo-websocket.ps1 @@ -0,0 +1,20 @@ +Send-PSUHarnessMessage -MessageType 'setState' -Data @{ + componentId = 'root-text' + state = @{ + text = 'Updated from websocket event' + } +} + +Set-PSUHarnessDownload -Id 'sample-download' -FileName 'harness.txt' -Content 'Download generated by the PSU framework harness.' + +Send-PSUHarnessMessage -MessageType 'download' -Data @{ + id = 'sample-download' + fileName = 'harness.txt' +} + +@{ + ok = $true + endpoint = $HarnessContext.EndpointId + eventName = $HarnessContext.EventName + eventData = $HarnessContext.EventData +} diff --git a/Apps/Frameworks/Harness/sample/harness.ps1 b/Apps/Frameworks/Harness/sample/harness.ps1 new file mode 100644 index 0000000..5ca89c0 --- /dev/null +++ b/Apps/Frameworks/Harness/sample/harness.ps1 @@ -0,0 +1,55 @@ +function Get-AntDesignHarnessEntryPoint { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$DistPath, + + [Parameter(Mandatory)] + [string]$BasePath + ) + + $manifestPath = Join-Path -Path $DistPath -ChildPath 'manifest.json' + + if (-not (Test-Path -Path $manifestPath)) { + throw "Ant Design framework manifest not found at '$manifestPath'. Run npm run build in Apps/Frameworks/AntDesign first." + } + + $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json -AsHashtable + $entry = $manifest['index.html'] + + if (-not $entry) { + throw "Ant Design framework manifest entry for 'index.html' was not found in '$manifestPath'." + } + + $styles = @() + + if ($entry.ContainsKey('css') -and $entry.css.Count -gt 0) { + $styles = @("$BasePath/$($entry.css[0] -replace '\\', '/')") + } + + @{ + Scripts = @("$BasePath/$($entry.file -replace '\\', '/')") + Styles = $styles + } +} + +$antDesignDistPath = Join-Path $HarnessScriptRoot '..\..\AntDesign\dist' +$antDesignAssetBasePath = '/frameworks/ant-design' +$antDesignEntryPoint = Get-AntDesignHarnessEntryPoint -DistPath $antDesignDistPath -BasePath $antDesignAssetBasePath + +@{ + DashboardScript = Join-Path $HarnessScriptRoot 'dashboard.ps1' + EndpointRoot = Join-Path $HarnessScriptRoot 'endpoints' + StaticAssets = @( + @{ + RequestPath = $antDesignAssetBasePath + Path = $antDesignDistPath + } + ) + Shell = @{ + Title = 'PSU Framework Harness' + MountId = 'root' + Scripts = $antDesignEntryPoint.Scripts + Styles = $antDesignEntryPoint.Styles + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Hubs/DashboardHub.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Hubs/DashboardHub.cs new file mode 100644 index 0000000..57ac85c --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Hubs/DashboardHub.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.SignalR; +using PowerShellUniversal.Frameworks.Harness.Models; +using PowerShellUniversal.Frameworks.Harness.Services; + +namespace PowerShellUniversal.Frameworks.Harness.Hubs; + +public sealed class DashboardHub( + HarnessDefinitionProvider definitionProvider, + HarnessEndpointRegistry endpointRegistry, + HarnessPowerShellService powerShellService, + HarnessRealtimeService realtimeService, + HarnessObjectNormalizer normalizer, + ILogger logger) : Hub +{ + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + var dashboardId = httpContext?.Request.Query["dashboardid"].ToString() ?? string.Empty; + var sessionId = httpContext?.Request.Query["sessionid"].ToString() ?? string.Empty; + var pageId = httpContext?.Request.Query["pageid"].ToString() ?? string.Empty; + var timezone = httpContext?.Request.Query["timezone"].ToString(); + + realtimeService.RegisterConnection(new HarnessConnection( + ConnectionId: Context.ConnectionId, + DashboardId: dashboardId, + SessionId: sessionId, + PageId: pageId, + Timezone: timezone, + ConnectedAt: DateTimeOffset.UtcNow)); + + if (!string.IsNullOrWhiteSpace(dashboardId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, dashboardId); + } + + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + realtimeService.RemoveConnection(Context.ConnectionId); + await base.OnDisconnectedAsync(exception); + } + + public async Task Event(HarnessClientEvent item) + { + await ExecuteClientEventAsync(item); + } + + public async Task ClientEvent(string eventId, string eventName, string? eventData, string? location) + { + await ExecuteClientEventAsync(new HarnessClientEvent + { + EventId = eventId, + EventName = eventName, + EventData = eventData, + Location = location + }); + } + + public Task GetState(string requestId, string state) + { + realtimeService.StoreSessionState(requestId, state); + return Task.CompletedTask; + } + + public Task WriteLog(HarnessLogMessage message) + { + logger.LogInformation("Harness client log: {Level} {Message}", message.Level, message.Message); + return Task.CompletedTask; + } + + private async Task ExecuteClientEventAsync(HarnessClientEvent item) + { + var definition = definitionProvider.GetDefinition(); + var endpointId = string.IsNullOrWhiteSpace(item.EventId) ? item.EventName : item.EventId; + if (!definition.TryResolveEndpointScript(endpointId, out var scriptPath) + && !endpointRegistry.TryResolveEndpointScript(endpointId, out scriptPath)) + { + logger.LogWarning("No websocket endpoint script was configured for {EndpointId}", endpointId); + return; + } + + var httpContext = Context.GetHttpContext(); + var invocationContext = new HarnessInvocationContext + { + DashboardId = httpContext?.Request.Query["dashboardid"].ToString() ?? string.Empty, + SessionId = httpContext?.Request.Query["sessionid"].ToString() ?? string.Empty, + PageId = httpContext?.Request.Query["pageid"].ToString() ?? string.Empty, + ConnectionId = Context.ConnectionId, + EndpointId = endpointId, + EventName = item.EventName, + EventData = normalizer.NormalizeForJson(item.EventData), + Location = item.Location, + Method = "WEBSOCKET", + Query = httpContext?.Request.Query.ToDictionary( + pair => pair.Key, + pair => pair.Value.Count == 1 ? (object?)pair.Value[0] : pair.Value.ToArray(), + StringComparer.OrdinalIgnoreCase) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase), + Headers = httpContext?.Request.Headers.ToDictionary( + pair => pair.Key, + pair => (string?)pair.Value.FirstOrDefault(), + StringComparer.OrdinalIgnoreCase) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase), + Cookies = httpContext?.Request.Cookies.ToDictionary( + pair => pair.Key, + pair => (string?)pair.Value, + StringComparer.OrdinalIgnoreCase) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase), + Form = new Dictionary(StringComparer.OrdinalIgnoreCase), + Body = null, + JsonBody = normalizer.NormalizeForJson(item.EventData), + Files = Array.Empty() + }; + + await powerShellService.InvokeEndpointAsync(scriptPath, invocationContext); + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Models/HarnessModels.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Models/HarnessModels.cs new file mode 100644 index 0000000..b87f451 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Models/HarnessModels.cs @@ -0,0 +1,151 @@ +namespace PowerShellUniversal.Frameworks.Harness.Models; + +public sealed class HarnessOptions +{ + public string DefinitionPath { get; set; } = "../../sample/harness.ps1"; + + public string DashboardId { get; set; } = "harness-dashboard"; + + public string PageId { get; set; } = "home"; + + public string SessionCookieName { get; set; } = "psu-harness-session-id"; +} + +public sealed record HarnessStaticAsset(string RequestPath, string PhysicalPath); + +public sealed record HarnessShellDefinition( + string Title, + string MountId, + IReadOnlyList Scripts, + IReadOnlyList Styles); + +public sealed record HarnessDefinition( + string SourcePath, + string? DashboardScriptPath, + string? EndpointRootPath, + IReadOnlyDictionary EndpointScripts, + IReadOnlyList StaticAssets, + HarnessShellDefinition Shell) +{ + public bool TryResolveEndpointScript(string endpointId, out string scriptPath) + { + if (EndpointScripts.TryGetValue(endpointId, out var configuredScript) && !string.IsNullOrWhiteSpace(configuredScript)) + { + scriptPath = configuredScript; + return true; + } + + if (!string.IsNullOrWhiteSpace(EndpointRootPath)) + { + var candidate = Path.Combine(EndpointRootPath, endpointId + ".ps1"); + if (File.Exists(candidate)) + { + scriptPath = candidate; + return true; + } + } + + scriptPath = string.Empty; + return false; + } +} + +public sealed class HarnessInvocationContext +{ + public required string DashboardId { get; init; } + + public required string SessionId { get; init; } + + public required string PageId { get; init; } + + public string? ConnectionId { get; init; } + + public string? EndpointId { get; init; } + + public string? EventName { get; init; } + + public object? EventData { get; init; } + + public string? Location { get; init; } + + public required string Method { get; init; } + + public required IReadOnlyDictionary Query { get; init; } + + public required IReadOnlyDictionary Headers { get; init; } + + public required IReadOnlyDictionary Cookies { get; init; } + + public required IReadOnlyDictionary Form { get; init; } + + public string? Body { get; init; } + + public object? JsonBody { get; init; } + + public required IReadOnlyList Files { get; init; } +} + +public sealed record HarnessUploadedFile( + string Name, + string FileName, + string ContentType, + long Length, + string TempPath); + +public sealed record HarnessConnection( + string ConnectionId, + string DashboardId, + string SessionId, + string PageId, + string? Timezone, + DateTimeOffset ConnectedAt); + +public sealed record HarnessDownload( + string Id, + string FileName, + string ContentType, + byte[] Content); + +public sealed class HarnessClientEvent +{ + public string? Type { get; init; } + + public string EventId { get; init; } = string.Empty; + + public string EventName { get; init; } = string.Empty; + + public object? EventData { get; init; } + + public string? Location { get; init; } +} + +public sealed class HarnessLogMessage +{ + public string? Scope { get; init; } + + public string? Feature { get; init; } + + public string? Message { get; init; } + + public string? Level { get; init; } +} + +public sealed class HarnessMessageRequest +{ + public string MessageType { get; init; } = string.Empty; + + public object? Data { get; init; } + + public string? ConnectionId { get; init; } + + public string? DashboardId { get; init; } +} + +public sealed class HarnessDownloadRegistrationRequest +{ + public string FileName { get; init; } = string.Empty; + + public string Content { get; init; } = string.Empty; + + public string ContentType { get; init; } = "text/plain"; +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/PowerShellUniversal.Frameworks.Harness.csproj b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/PowerShellUniversal.Frameworks.Harness.csproj new file mode 100644 index 0000000..8c0499f --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/PowerShellUniversal.Frameworks.Harness.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Program.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Program.cs new file mode 100644 index 0000000..412b250 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Program.cs @@ -0,0 +1,361 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using PowerShellUniversal.Frameworks.Harness.Hubs; +using PowerShellUniversal.Frameworks.Harness.Models; +using PowerShellUniversal.Frameworks.Harness.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure(builder.Configuration.GetSection("Harness")); +builder.Services.AddSignalR().AddJsonProtocol(options => +{ + options.PayloadSerializerOptions.PropertyNamingPolicy = null; +}); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + policy + .SetIsOriginAllowed(_ => true) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials()); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.UseCors(); + +var definitionProvider = app.Services.GetRequiredService(); +var initialDefinition = definitionProvider.GetDefinition(); +foreach (var staticAsset in initialDefinition.StaticAssets) +{ + if (!Directory.Exists(staticAsset.PhysicalPath)) + { + app.Logger.LogWarning("Static asset path does not exist: {PhysicalPath}", staticAsset.PhysicalPath); + continue; + } + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(staticAsset.PhysicalPath), + RequestPath = staticAsset.RequestPath + }); +} + +app.MapGet("/", (HttpContext httpContext, HarnessDefinitionProvider provider, IOptions options) => +{ + var definition = provider.GetDefinition(); + var html = RenderShellPage(httpContext, definition, options.Value); + return Results.Content(html, "text/html", Encoding.UTF8); +}); + +app.MapGet("/api/internal/dashboard", async ( + HttpContext httpContext, + HarnessDefinitionProvider provider, + HarnessPowerShellService powerShellService, + IOptions options) => +{ + var definition = provider.GetDefinition(); + if (string.IsNullOrWhiteSpace(definition.DashboardScriptPath)) + { + return Results.Json(new + { + dashboard = new { type = "empty" }, + sessionId = GetOrCreateSessionId(httpContext, options.Value), + pageId = options.Value.PageId, + dashboardName = "PSU Framework Harness", + developerLicense = true + }); + } + + var context = await CreateInvocationContextAsync(httpContext, options.Value, endpointId: null, methodOverride: "GET /api/internal/dashboard"); + var bootstrap = await powerShellService.InvokeBootstrapAsync(definition.DashboardScriptPath, context); + return Results.Json(bootstrap); +}); + +app.MapMethods("/api/internal/component/element/{id}", new[] { "GET", "POST" }, async ( + HttpContext httpContext, + string id, + HarnessDefinitionProvider provider, + HarnessEndpointRegistry endpointRegistry, + HarnessPowerShellService powerShellService, + IOptions options) => +{ + var definition = provider.GetDefinition(); + if (!definition.TryResolveEndpointScript(id, out var scriptPath) + && !endpointRegistry.TryResolveEndpointScript(id, out scriptPath)) + { + return Results.Json(new { }); + } + + var context = await CreateInvocationContextAsync(httpContext, options.Value, endpointId: id); + var result = await powerShellService.InvokeEndpointAsync(scriptPath, context); + return ToEndpointResult(httpContext, result); +}); + +app.MapPost("/api/internal/component/element/sessionState/{requestId}", async ( + HttpContext httpContext, + string requestId, + HarnessRealtimeService realtimeService, + HarnessObjectNormalizer normalizer) => +{ + using var reader = new StreamReader(httpContext.Request.Body); + var body = await reader.ReadToEndAsync(); + if (normalizer.TryParseJson(body, out var jsonBody)) + { + realtimeService.StoreSessionState(requestId, jsonBody); + } + else + { + realtimeService.StoreSessionState(requestId, body); + } + + return Results.Json(new { message = "Session state set" }); +}); + +app.MapGet("/api/internal/dashboard/download/{dashboardId}/{id}", ( + string id, + HarnessRealtimeService realtimeService) => +{ + if (!realtimeService.TryGetDownload(id, out var download) || download is null) + { + return Results.NotFound(); + } + + return Results.File(download.Content, download.ContentType, download.FileName); +}); + +app.MapGet("/api/harness/connections", (HarnessRealtimeService realtimeService) => + Results.Json(realtimeService.GetConnections())); + +app.MapPost("/api/harness/messages", async ( + HarnessMessageRequest request, + HarnessRealtimeService realtimeService, + IOptions options) => +{ + await realtimeService.SendAsync( + request.MessageType, + request.Data, + request.ConnectionId, + string.IsNullOrWhiteSpace(request.DashboardId) ? options.Value.DashboardId : request.DashboardId); + + return Results.Json(new { message = "Message sent" }); +}); + +app.MapPost("/api/harness/downloads/{id}", ( + string id, + HarnessDownloadRegistrationRequest request, + HarnessRealtimeService realtimeService) => +{ + realtimeService.StoreDownload(new HarnessDownload( + Id: id, + FileName: request.FileName, + ContentType: request.ContentType, + Content: Encoding.UTF8.GetBytes(request.Content))); + + return Results.Json(new { message = "Download stored" }); +}); + +app.MapHub("/dashboardhub"); + +app.Run(); + +static IResult ToEndpointResult(HttpContext httpContext, object? result) +{ + if (result is string text && WantsPlainText(httpContext)) + { + return Results.Text(text, "text/plain", Encoding.UTF8); + } + + return Results.Json(result ?? new { }); +} + +static bool WantsPlainText(HttpContext httpContext) +{ + var accept = httpContext.Request.Headers.Accept.ToString(); + return accept.Contains("text/plain", StringComparison.OrdinalIgnoreCase); +} + +static string GetOrCreateSessionId(HttpContext httpContext, HarnessOptions options) +{ + if (httpContext.Request.Cookies.TryGetValue(options.SessionCookieName, out var existing) && !string.IsNullOrWhiteSpace(existing)) + { + return existing; + } + + var sessionId = Guid.NewGuid().ToString(); + httpContext.Response.Cookies.Append(options.SessionCookieName, sessionId, new CookieOptions + { + HttpOnly = true, + SameSite = SameSiteMode.Lax, + IsEssential = true + }); + return sessionId; +} + +static async Task CreateInvocationContextAsync( + HttpContext httpContext, + HarnessOptions options, + string? endpointId, + string? methodOverride = null) +{ + var normalizer = httpContext.RequestServices.GetRequiredService(); + var sessionId = GetOrCreateSessionId(httpContext, options); + + var query = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var pair in httpContext.Request.Query) + { + query[pair.Key] = pair.Value.Count == 1 ? pair.Value[0] : pair.Value.ToArray(); + } + + var headers = httpContext.Request.Headers.ToDictionary( + pair => pair.Key, + pair => (string?)pair.Value.FirstOrDefault(), + StringComparer.OrdinalIgnoreCase); + + var cookies = httpContext.Request.Cookies.ToDictionary( + pair => pair.Key, + pair => (string?)pair.Value, + StringComparer.OrdinalIgnoreCase); + + var form = new Dictionary(StringComparer.OrdinalIgnoreCase); + var files = new List(); + string? body = null; + object? jsonBody = null; + + if (httpContext.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase)) + { + if (httpContext.Request.HasFormContentType) + { + var formCollection = await httpContext.Request.ReadFormAsync(new FormOptions + { + BufferBody = true + }); + + foreach (var pair in formCollection) + { + form[pair.Key] = pair.Value.Count == 1 ? pair.Value[0] : pair.Value.ToArray(); + } + + foreach (var file in formCollection.Files) + { + var tempPath = Path.GetTempFileName(); + await using var stream = File.OpenWrite(tempPath); + await file.CopyToAsync(stream); + files.Add(new HarnessUploadedFile( + Name: file.Name, + FileName: file.FileName, + ContentType: file.ContentType, + Length: file.Length, + TempPath: tempPath)); + } + } + else + { + using var reader = new StreamReader(httpContext.Request.Body); + body = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(body) && normalizer.TryParseJson(body, out var parsed)) + { + jsonBody = parsed; + } + } + } + + return new HarnessInvocationContext + { + DashboardId = options.DashboardId, + SessionId = sessionId, + PageId = options.PageId, + ConnectionId = httpContext.Request.Headers["UDConnectionId"].FirstOrDefault(), + EndpointId = endpointId, + EventName = null, + EventData = null, + Location = null, + Method = methodOverride ?? httpContext.Request.Method, + Query = query, + Headers = headers, + Cookies = cookies, + Form = form, + Body = body, + JsonBody = jsonBody, + Files = files + }; +} + +static string RenderShellPage(HttpContext httpContext, HarnessDefinition definition, HarnessOptions options) +{ + var baseUrl = WebUtility.HtmlEncode(httpContext.Request.PathBase.Value ?? string.Empty); + var dashboardId = WebUtility.HtmlEncode(options.DashboardId); + var title = WebUtility.HtmlEncode(definition.Shell.Title); + var mountId = WebUtility.HtmlEncode(definition.Shell.MountId); + var styles = string.Join(Environment.NewLine, definition.Shell.Styles.Select(style => + $" ")); + var scripts = string.Join(Environment.NewLine, definition.Shell.Scripts.Select(script => + $" ")); + + var body = definition.Shell.Scripts.Count > 0 + ? $"
" + : $"

{title}

No shell scripts were configured in {WebUtility.HtmlEncode(definition.SourcePath)}.

"; + + return $$""" + + + + + + + + {{title}} + +{{styles}} + +{{scripts}} + + + {{body}} + + +"""; +} \ No newline at end of file diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessDefinitionProvider.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessDefinitionProvider.cs new file mode 100644 index 0000000..df5a930 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessDefinitionProvider.cs @@ -0,0 +1,180 @@ +using System.Management.Automation; +using Microsoft.Extensions.Options; +using PowerShellUniversal.Frameworks.Harness.Models; + +namespace PowerShellUniversal.Frameworks.Harness.Services; + +public sealed class HarnessDefinitionProvider( + IOptions options, + IHostEnvironment hostEnvironment, + HarnessObjectNormalizer normalizer, + ILogger logger) +{ + private readonly object _syncLock = new(); + private HarnessDefinition? _cachedDefinition; + private DateTime _cachedWriteTimeUtc; + + public HarnessDefinition GetDefinition() + { + var definitionPath = ResolvePath(hostEnvironment.ContentRootPath, options.Value.DefinitionPath); + var writeTimeUtc = File.GetLastWriteTimeUtc(definitionPath); + + lock (_syncLock) + { + if (_cachedDefinition is not null && writeTimeUtc == _cachedWriteTimeUtc) + { + return _cachedDefinition; + } + + _cachedDefinition = LoadDefinition(definitionPath); + _cachedWriteTimeUtc = writeTimeUtc; + return _cachedDefinition; + } + } + + private HarnessDefinition LoadDefinition(string definitionPath) + { + if (!File.Exists(definitionPath)) + { + throw new FileNotFoundException($"Harness definition script was not found: {definitionPath}", definitionPath); + } + + logger.LogInformation("Loading harness definition from {DefinitionPath}", definitionPath); + + using var powershell = PowerShell.Create(); + var definitionScript = File.ReadAllText(definitionPath); + var definitionDirectory = Path.GetDirectoryName(definitionPath) ?? hostEnvironment.ContentRootPath; + powershell.Runspace.SessionStateProxy.SetVariable("PSScriptRoot", definitionDirectory); + powershell.Runspace.SessionStateProxy.SetVariable("PSCommandPath", definitionPath); + powershell.Runspace.SessionStateProxy.SetVariable("HarnessScriptRoot", definitionDirectory); + powershell.Runspace.SessionStateProxy.SetVariable("HarnessScriptPath", definitionPath); + powershell.AddScript($"Set-Location -LiteralPath '{definitionDirectory.Replace("'", "''")}'", useLocalScope: false); + powershell.AddScript(definitionScript, useLocalScope: false); + var results = powershell.Invoke(); + + if (powershell.HadErrors) + { + var errorText = string.Join(Environment.NewLine, powershell.Streams.Error.Select(error => error.ToString())); + throw new InvalidOperationException($"Failed to load harness definition.{Environment.NewLine}{errorText}"); + } + + var normalized = normalizer.NormalizeForJson(results.Count switch + { + 0 => null, + 1 => results[0], + _ => results + }); + + if (normalized is not IReadOnlyDictionary root) + { + throw new InvalidOperationException("Harness definition script must return a hashtable-like object."); + } + + var dashboardScriptPath = GetOptionalPath(root, definitionDirectory, "DashboardScript", "Dashboard"); + var endpointRootPath = GetOptionalPath(root, definitionDirectory, "EndpointRoot"); + var endpointScripts = GetDictionary(root, "Endpoints") + .ToDictionary( + pair => pair.Key, + pair => ResolvePath(definitionDirectory, Convert.ToString(pair.Value) ?? string.Empty), + StringComparer.OrdinalIgnoreCase); + + var staticAssets = GetArray(root, "StaticAssets") + .OfType>() + .Select(asset => new HarnessStaticAsset( + RequestPath: Convert.ToString(asset.GetValueOrDefault("RequestPath")) ?? string.Empty, + PhysicalPath: ResolvePath(definitionDirectory, Convert.ToString(asset.GetValueOrDefault("Path")) ?? string.Empty))) + .Where(asset => !string.IsNullOrWhiteSpace(asset.RequestPath) && !string.IsNullOrWhiteSpace(asset.PhysicalPath)) + .ToArray(); + + var shell = GetDictionary(root, "Shell"); + var shellDefinition = new HarnessShellDefinition( + Title: Convert.ToString(shell.GetValueOrDefault("Title")) ?? "PSU Framework Harness", + MountId: Convert.ToString(shell.GetValueOrDefault("MountId")) ?? "root", + Scripts: GetStringArray(shell, "Scripts"), + Styles: GetStringArray(shell, "Styles")); + + return new HarnessDefinition( + SourcePath: definitionPath, + DashboardScriptPath: dashboardScriptPath, + EndpointRootPath: endpointRootPath, + EndpointScripts: endpointScripts, + StaticAssets: staticAssets, + Shell: shellDefinition); + } + + private static IReadOnlyDictionary GetDictionary(IReadOnlyDictionary source, params string[] keys) + { + foreach (var key in keys) + { + if (source.TryGetValue(key, out var value) && value is IReadOnlyDictionary dictionary) + { + return dictionary; + } + } + + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + private static IReadOnlyList GetArray(IReadOnlyDictionary source, params string[] keys) + { + foreach (var key in keys) + { + if (!source.TryGetValue(key, out var value) || value is null) + { + continue; + } + + if (value is IReadOnlyList list) + { + return list; + } + + if (value is IEnumerable enumerable) + { + return enumerable.ToArray(); + } + + return new[] { value }; + } + + return Array.Empty(); + } + + private static IReadOnlyList GetStringArray(IReadOnlyDictionary source, params string[] keys) + { + return GetArray(source, keys) + .Select(item => Convert.ToString(item)) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Cast() + .ToArray(); + } + + private static string? GetOptionalPath(IReadOnlyDictionary source, string basePath, params string[] keys) + { + foreach (var key in keys) + { + if (!source.TryGetValue(key, out var value)) + { + continue; + } + + var stringValue = Convert.ToString(value); + if (!string.IsNullOrWhiteSpace(stringValue)) + { + return ResolvePath(basePath, stringValue); + } + } + + return null; + } + + private static string ResolvePath(string basePath, string value) + { + if (Path.IsPathRooted(value)) + { + return Path.GetFullPath(value); + } + + return Path.GetFullPath(Path.Combine(basePath, value)); + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessEndpointRegistry.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessEndpointRegistry.cs new file mode 100644 index 0000000..8375523 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessEndpointRegistry.cs @@ -0,0 +1,64 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; + +namespace PowerShellUniversal.Frameworks.Harness.Services; + +public sealed class HarnessEndpointRegistry +{ + private readonly ConcurrentDictionary scriptPaths = new(StringComparer.OrdinalIgnoreCase); + private readonly string endpointRoot = Path.Combine( + Path.GetTempPath(), + "psu-framework-harness", + "inline-endpoints", + Guid.NewGuid().ToString("n")); + + public string RegisterEndpoint(string componentId, string scriptContent, IEnumerable? modulePaths) + { + if (string.IsNullOrWhiteSpace(scriptContent)) + { + throw new ArgumentException("Inline endpoint script content cannot be empty.", nameof(scriptContent)); + } + + Directory.CreateDirectory(endpointRoot); + + var endpointId = $"{SanitizeComponentId(componentId)}-{Convert.ToHexString(RandomNumberGenerator.GetBytes(6)).ToLowerInvariant()}"; + var scriptPath = Path.Combine(endpointRoot, endpointId + ".ps1"); + + var builder = new StringBuilder(); + foreach (var modulePath in (modulePaths ?? Array.Empty()) + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Distinct(StringComparer.OrdinalIgnoreCase)) + { + builder.Append("Import-Module -Name '"); + builder.Append(modulePath.Replace("'", "''")); + builder.AppendLine("' -Force"); + } + + builder.AppendLine(); + builder.AppendLine(scriptContent); + + File.WriteAllText(scriptPath, builder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + scriptPaths[endpointId] = scriptPath; + return endpointId; + } + + public bool TryResolveEndpointScript(string endpointId, out string scriptPath) + { + return scriptPaths.TryGetValue(endpointId, out scriptPath!); + } + + private static string SanitizeComponentId(string componentId) + { + if (string.IsNullOrWhiteSpace(componentId)) + { + return "endpoint"; + } + + var characters = componentId + .Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '-') + .ToArray(); + + return new string(characters).Trim('-'); + } +} \ No newline at end of file diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessObjectNormalizer.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessObjectNormalizer.cs new file mode 100644 index 0000000..ab45473 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessObjectNormalizer.cs @@ -0,0 +1,199 @@ +using System.Collections; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Management.Automation; + +namespace PowerShellUniversal.Frameworks.Harness.Services; + +public sealed class HarnessObjectNormalizer +{ + public object? NormalizeForJson(object? value) + { + return value switch + { + null => null, + JsonElement element => NormalizeJsonElement(element), + JsonNode node => node.Deserialize(), + PSObject psObject => NormalizePsObject(psObject), + IDictionary dictionary => NormalizeDictionary(dictionary), + byte[] bytes => Convert.ToBase64String(bytes), + IEnumerable enumerable when value is not string => NormalizeEnumerable(enumerable), + Enum enumValue => enumValue.ToString(), + _ when IsSimple(value.GetType()) => value, + _ => NormalizeObject(value) + }; + } + + public bool TryParseJson(string input, out object? value) + { + try + { + using var document = JsonDocument.Parse(input); + value = NormalizeJsonElement(document.RootElement); + return true; + } + catch (JsonException) + { + value = null; + return false; + } + } + + private object? NormalizePsObject(PSObject psObject) + { + var baseObject = psObject.BaseObject; + + if (!ReferenceEquals(baseObject, psObject) && baseObject is not PSCustomObject) + { + return NormalizeForJson(baseObject); + } + + if (!psObject.Properties.Any()) + { + return baseObject is null || ReferenceEquals(baseObject, psObject) + ? null + : NormalizeForJson(baseObject); + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var property in psObject.Properties) + { + if (!property.IsGettable) + { + continue; + } + + result[property.Name] = NormalizeForJson(property.Value); + } + + return result; + } + + private static bool IsSimple(Type type) + { + return type.IsPrimitive + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(DateTime) + || type == typeof(DateTimeOffset) + || type == typeof(Guid) + || type == typeof(TimeSpan) + || type == typeof(Uri); + } + + private object NormalizeDictionary(IDictionary dictionary) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (DictionaryEntry entry in dictionary) + { + var key = Convert.ToString(entry.Key); + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + result[key] = NormalizeForJson(entry.Value); + } + + return result; + } + + private object NormalizeEnumerable(IEnumerable enumerable) + { + var result = new List(); + foreach (var item in enumerable) + { + result.Add(NormalizeForJson(item)); + } + + return result; + } + + private object NormalizeObject(object value) + { + if (LooksLikeEndpointDescriptor(value)) + { + return NormalizeEndpointDescriptor(value); + } + + var properties = value + .GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(property => property.CanRead) + .ToArray(); + + if (properties.Length == 0) + { + return value.ToString() ?? string.Empty; + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var property in properties) + { + result[property.Name] = NormalizeForJson(property.GetValue(value)); + } + + return result; + } + + private object NormalizeEndpointDescriptor(object value) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + AddEndpointProperty(result, value, "endpoint"); + AddEndpointProperty(result, value, "name"); + AddEndpointProperty(result, value, "accept"); + AddEndpointProperty(result, value, "contentType"); + AddEndpointProperty(result, value, "websocket"); + AddEndpointProperty(result, value, "javaScript"); + return result; + } + + private static bool LooksLikeEndpointDescriptor(object value) + { + var properties = value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + return properties.Any(property => string.Equals(property.Name, "endpoint", StringComparison.OrdinalIgnoreCase)) + && properties.Any(property => string.Equals(property.Name, "name", StringComparison.OrdinalIgnoreCase)); + } + + private void AddEndpointProperty(Dictionary result, object value, string propertyName) + { + var property = value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(candidate => string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase)); + + if (property is null || !property.CanRead) + { + return; + } + + var propertyValue = property.GetValue(value); + if (propertyValue is null) + { + return; + } + + result[propertyName] = NormalizeForJson(propertyValue); + } + + private object? NormalizeJsonElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Object => element.EnumerateObject().ToDictionary( + property => property.Name, + property => NormalizeJsonElement(property.Value), + StringComparer.OrdinalIgnoreCase), + JsonValueKind.Array => element.EnumerateArray().Select(NormalizeJsonElement).ToList(), + JsonValueKind.String when element.TryGetDateTimeOffset(out var dto) => dto, + JsonValueKind.String when element.TryGetGuid(out var guid) => guid, + JsonValueKind.String => element.GetString(), + JsonValueKind.Number when element.TryGetInt64(out var int64) => int64, + JsonValueKind.Number => element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.ToString() + }; + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessPowerShellService.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessPowerShellService.cs new file mode 100644 index 0000000..3cb0315 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessPowerShellService.cs @@ -0,0 +1,316 @@ +using System.Text; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using PowerShellUniversal.Frameworks.Harness.Models; + +namespace PowerShellUniversal.Frameworks.Harness.Services; + +public sealed class HarnessPowerShellService( + HarnessEndpointRegistry endpointRegistry, + HarnessRealtimeService realtimeService, + HarnessObjectNormalizer normalizer, + ILogger logger) +{ + private readonly HarnessEndpointRegistry endpointRegistry = endpointRegistry; + + private const string HarnessPrelude = """ +$Body = $HarnessContext.Body +$EventData = if ($null -ne $HarnessContext.EventData) { + $HarnessContext.EventData +} +elseif ($null -ne $HarnessContext.JsonBody) { + $HarnessContext.JsonBody +} +else { + $HarnessContext.Body +} + +$ConnectionId = $HarnessContext.ConnectionId +$DashboardId = $HarnessContext.DashboardId +$SessionId = $HarnessContext.SessionId +$PageId = $HarnessContext.PageId + +$DashboardHub = [pscustomobject]@{} +$DashboardHub | Add-Member -MemberType ScriptMethod -Name SendWebSocketMessage -Value { + param( + [Parameter(Mandatory)] + [object]$Arg1, + + [Parameter(Mandatory)] + [object]$Arg2, + + [Parameter()] + [object]$Arg3 + ) + + if ($PSBoundParameters.ContainsKey('Arg3')) { + $PsuHarness.SendMessage([string]$Arg2, $Arg3, [string]$Arg1, $DashboardId) + return + } + + $PsuHarness.SendMessage([string]$Arg1, $Arg2, $null, $DashboardId) +} + +$DashboardHub | Add-Member -MemberType ScriptMethod -Name SendMessage -Value { + param( + [Parameter(Mandatory)] + [string]$MessageType, + + [Parameter()] + $Data, + + [Parameter()] + [string]$ConnectionId, + + [Parameter()] + [string]$DashboardId + ) + + $PsuHarness.SendMessage($MessageType, $Data, $ConnectionId, $DashboardId) +} + +class Endpoint { + static [object]$Registry + [bool]$endpoint = $true + [string]$name + [string]$accept + [string]$contentType + hidden [scriptblock]$scriptBlock + + Endpoint([scriptblock]$scriptBlock) { + $this.scriptBlock = $scriptBlock + } + + [void] Register([string]$Id, [object]$Cmdlet) { + if ($null -eq $this.scriptBlock) { + throw 'Endpoint requires a script block.' + } + + if (-not [string]::IsNullOrWhiteSpace($this.name)) { + return + } + + if ($null -eq [Endpoint]::Registry) { + throw 'Endpoint registry has not been initialized for this runspace.' + } + + $modulePaths = @( + Get-Module | + Where-Object { -not [string]::IsNullOrWhiteSpace($_.Path) } | + Select-Object -ExpandProperty Path -Unique + ) + + $this.name = [Endpoint]::Registry.RegisterEndpoint($Id, $this.scriptBlock.ToString(), $modulePaths) + } +} + +[Endpoint]::Registry = $PsuHarness + +function Send-PSUHarnessMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$MessageType, + + [Parameter()] + $Data, + + [Parameter()] + [string]$ConnectionId, + + [Parameter()] + [string]$DashboardId + ) + + $PsuHarness.SendMessage($MessageType, $Data, $ConnectionId, $DashboardId) +} + +function Set-PSUHarnessDownload { + [CmdletBinding(DefaultParameterSetName = 'Content')] + param( + [Parameter(Mandatory)] + [string]$Id, + + [Parameter(Mandatory)] + [string]$FileName, + + [Parameter(ParameterSetName = 'Content', Mandatory)] + [string]$Content, + + [Parameter(ParameterSetName = 'File', Mandatory)] + [string]$Path, + + [Parameter()] + [string]$ContentType = 'text/plain' + ) + + if ($PSCmdlet.ParameterSetName -eq 'File') { + $PsuHarness.SetDownloadFromFile($Id, $FileName, $Path, $ContentType) + return + } + + $PsuHarness.SetDownloadText($Id, $FileName, $Content, $ContentType) +} + +function Set-PSUHarnessSessionState { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$RequestId, + + [Parameter()] + $State + ) + + $PsuHarness.SetSessionState($RequestId, $State) +} + +function Get-PSUHarnessConnections { + $PsuHarness.GetConnections() +} + +function Show-UDToast { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Message, + + [ValidateSet('info', 'success', 'warning', 'error')] + [string]$Type = 'info', + + [double]$Duration = 4.5 + ) + + Send-PSUHarnessMessage -MessageType 'toast' -Data @{ + message = $Message + type = $Type + duration = $Duration + } +} +"""; + + public async Task InvokeBootstrapAsync(string scriptPath, HarnessInvocationContext context) + { + var output = await InvokeScriptAsync(scriptPath, context); + return EnsureBootstrapShape(output, context); + } + + public Task InvokeEndpointAsync(string scriptPath, HarnessInvocationContext context) + { + return InvokeScriptAsync(scriptPath, context); + } + + private async Task InvokeScriptAsync(string scriptPath, HarnessInvocationContext context) + { + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException($"Harness PowerShell script was not found: {scriptPath}", scriptPath); + } + + logger.LogInformation("Executing PowerShell harness script {ScriptPath}", scriptPath); + + return await Task.Run(() => + { + var initialSessionState = InitialSessionState.CreateDefault2(); + initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass; + + using var runspace = RunspaceFactory.CreateRunspace(initialSessionState); + runspace.Open(); + + using var powershell = PowerShell.Create(); + powershell.Runspace = runspace; + var scriptDirectory = Path.GetDirectoryName(scriptPath) ?? Directory.GetCurrentDirectory(); + var scriptContent = File.ReadAllText(scriptPath); + powershell.Runspace.SessionStateProxy.SetVariable("PSScriptRoot", scriptDirectory); + powershell.Runspace.SessionStateProxy.SetVariable("PSCommandPath", scriptPath); + powershell.Runspace.SessionStateProxy.SetVariable("HarnessScriptRoot", scriptDirectory); + powershell.Runspace.SessionStateProxy.SetVariable("HarnessScriptPath", scriptPath); + powershell.Runspace.SessionStateProxy.SetVariable("HarnessContext", context); + powershell.Runspace.SessionStateProxy.SetVariable("PsuHarness", new PowerShellHarnessApi(this, realtimeService, context)); + powershell.AddScript(HarnessPrelude, useLocalScope: false); + powershell.AddScript($"Set-Location -LiteralPath '{scriptDirectory.Replace("'", "''")}'", useLocalScope: false); + powershell.AddScript(scriptContent, useLocalScope: false); + + var results = powershell.Invoke(); + if (powershell.HadErrors) + { + var errorText = string.Join(Environment.NewLine, powershell.Streams.Error.Select(error => error.ToString())); + throw new InvalidOperationException($"Harness PowerShell execution failed.{Environment.NewLine}{errorText}"); + } + + return normalizer.NormalizeForJson(results.Count switch + { + 0 => null, + 1 => results[0], + _ => results + }); + }); + } + + private object EnsureBootstrapShape(object? output, HarnessInvocationContext context) + { + Dictionary bootstrap; + if (output is IReadOnlyDictionary dictionary && dictionary.ContainsKey("dashboard")) + { + bootstrap = new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + else + { + bootstrap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["dashboard"] = output ?? new Dictionary(StringComparer.OrdinalIgnoreCase) + }; + } + + bootstrap.TryAdd("sessionId", context.SessionId); + bootstrap.TryAdd("pageId", context.PageId); + bootstrap.TryAdd("dashboardName", "PSU Framework Harness"); + bootstrap.TryAdd("developerLicense", true); + + return bootstrap; + } + + private sealed class PowerShellHarnessApi(HarnessPowerShellService outer, HarnessRealtimeService realtimeService, HarnessInvocationContext context) + { + public void SendMessage(string messageType, object? data, string? connectionId, string? dashboardId) + { + realtimeService + .SendAsync(messageType, data, connectionId, string.IsNullOrWhiteSpace(dashboardId) ? context.DashboardId : dashboardId) + .GetAwaiter() + .GetResult(); + } + + public void SetDownloadText(string id, string fileName, string content, string contentType) + { + realtimeService.StoreDownload(new HarnessDownload( + Id: id, + FileName: fileName, + ContentType: contentType, + Content: Encoding.UTF8.GetBytes(content))); + } + + public void SetDownloadFromFile(string id, string fileName, string path, string contentType) + { + realtimeService.StoreDownload(new HarnessDownload( + Id: id, + FileName: fileName, + ContentType: contentType, + Content: File.ReadAllBytes(path))); + } + + public void SetSessionState(string requestId, object? state) + { + realtimeService.StoreSessionState(requestId, state); + } + + public IReadOnlyList GetConnections() + { + return realtimeService.GetConnections(); + } + + public string RegisterEndpoint(string componentId, string scriptContent, string[]? modulePaths) + { + return outer.endpointRegistry.RegisterEndpoint(componentId, scriptContent, modulePaths); + } + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessRealtimeService.cs b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessRealtimeService.cs new file mode 100644 index 0000000..93c2632 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/Services/HarnessRealtimeService.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.SignalR; +using PowerShellUniversal.Frameworks.Harness.Hubs; +using PowerShellUniversal.Frameworks.Harness.Models; + +namespace PowerShellUniversal.Frameworks.Harness.Services; + +public sealed class HarnessRealtimeService( + IHubContext hubContext, + HarnessObjectNormalizer normalizer) +{ + private readonly ConcurrentDictionary _connections = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _sessionState = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _downloads = new(StringComparer.OrdinalIgnoreCase); + + public void RegisterConnection(HarnessConnection connection) + { + _connections[connection.ConnectionId] = connection; + } + + public void RemoveConnection(string connectionId) + { + _connections.TryRemove(connectionId, out _); + } + + public IReadOnlyList GetConnections() + { + return _connections.Values + .OrderBy(connection => connection.ConnectedAt) + .ToArray(); + } + + public async Task SendAsync(string messageType, object? data = null, string? connectionId = null, string? dashboardId = null) + { + var clientProxy = !string.IsNullOrWhiteSpace(connectionId) + ? hubContext.Clients.Client(connectionId) + : !string.IsNullOrWhiteSpace(dashboardId) + ? hubContext.Clients.Group(dashboardId) + : hubContext.Clients.All; + + if (data is null) + { + await clientProxy.SendAsync(messageType); + return; + } + + await clientProxy.SendAsync(messageType, normalizer.NormalizeForJson(data)); + } + + public void StoreSessionState(string requestId, object? state) + { + _sessionState[requestId] = normalizer.NormalizeForJson(state); + } + + public bool TryGetSessionState(string requestId, out object? state) + { + return _sessionState.TryGetValue(requestId, out state); + } + + public void StoreDownload(HarnessDownload download) + { + _downloads[download.Id] = download; + } + + public bool TryGetDownload(string id, out HarnessDownload? download) + { + var found = _downloads.TryGetValue(id, out var value); + download = value; + return found; + } +} diff --git a/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/appsettings.json b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/appsettings.json new file mode 100644 index 0000000..2f31c32 --- /dev/null +++ b/Apps/Frameworks/Harness/src/PowerShellUniversal.Frameworks.Harness/appsettings.json @@ -0,0 +1,14 @@ +{ + "Harness": { + "DefinitionPath": "../../sample/harness.ps1", + "DashboardId": "harness-dashboard", + "PageId": "home", + "SessionCookieName": "psu-harness-session-id" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Apps/Frameworks/Start-Framework.ps1 b/Apps/Frameworks/Start-Framework.ps1 new file mode 100644 index 0000000..c3ec5e2 --- /dev/null +++ b/Apps/Frameworks/Start-Framework.ps1 @@ -0,0 +1,155 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory, Position = 0)] + [string]$Framework, + + [switch]$Build, + + [int]$Port = 5057, + + [switch]$NoBrowser, + + [switch]$PassThru +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Resolve-FrameworkDirectory { + param( + [Parameter(Mandatory)] + [string]$FrameworkPath, + + [Parameter(Mandatory)] + [string]$RootPath + ) + + $resolved = $null + + try { + $resolved = Resolve-Path -LiteralPath $FrameworkPath -ErrorAction Stop + } + catch { + $combinedPath = Join-Path $RootPath $FrameworkPath + $resolved = Resolve-Path -LiteralPath $combinedPath -ErrorAction Stop + } + + $frameworkDirectory = $resolved.ProviderPath + if (-not (Test-Path -LiteralPath $frameworkDirectory -PathType Container)) { + throw "Framework path must resolve to a directory: $FrameworkPath" + } + + return $frameworkDirectory +} + +function Invoke-FrameworkBuild { + param( + [Parameter(Mandatory)] + [string]$FrameworkDirectory + ) + + $packageJson = Join-Path $FrameworkDirectory 'package.json' + if (Test-Path -LiteralPath $packageJson) { + Write-Host "Building framework with npm run build in $FrameworkDirectory" + Push-Location $FrameworkDirectory + try { + & npm run build + if ($LASTEXITCODE -ne 0) { + throw "npm run build failed for $FrameworkDirectory" + } + } + finally { + Pop-Location + } + + return + } + + $buildScript = Join-Path $FrameworkDirectory 'build.ps1' + if (Test-Path -LiteralPath $buildScript) { + Write-Host "Building framework with $buildScript" + & $buildScript + return + } + + $project = Get-ChildItem -LiteralPath $FrameworkDirectory -Filter *.csproj -File | Select-Object -First 1 + if ($null -ne $project) { + Write-Host "Building framework with dotnet build $($project.FullName)" + & dotnet build $project.FullName + if ($LASTEXITCODE -ne 0) { + throw "dotnet build failed for $($project.FullName)" + } + + return + } + + throw "No supported build surface was found in $FrameworkDirectory. Expected package.json, build.ps1, or a .csproj." +} + +$frameworkRoot = Split-Path -Parent $PSCommandPath +$frameworkDirectory = Resolve-FrameworkDirectory -FrameworkPath $Framework -RootPath $frameworkRoot +$frameworkName = Split-Path -Leaf $frameworkDirectory +$definitionPath = Join-Path $frameworkDirectory 'harness.ps1' + +if (-not (Test-Path -LiteralPath $definitionPath -PathType Leaf)) { + throw "Framework harness definition was not found: $definitionPath" +} + +if ($Build) { + Invoke-FrameworkBuild -FrameworkDirectory $frameworkDirectory +} + +$harnessProject = Join-Path $frameworkRoot 'Harness\src\PowerShellUniversal.Frameworks.Harness\PowerShellUniversal.Frameworks.Harness.csproj' +if (-not (Test-Path -LiteralPath $harnessProject -PathType Leaf)) { + throw "Harness project was not found: $harnessProject" +} + +$url = "http://127.0.0.1:$Port" +$dotnetArguments = @( + 'run' + '--project' + $harnessProject + '--urls' + $url + '--' + "--Harness:DefinitionPath=$definitionPath" +) + +Write-Host "Starting $frameworkName on $url" +$process = Start-Process -FilePath 'dotnet' -ArgumentList $dotnetArguments -WorkingDirectory $frameworkRoot -PassThru + +$deadline = (Get-Date).AddSeconds(30) +$isReady = $false +while ((Get-Date) -lt $deadline) { + if ($process.HasExited) { + throw "$frameworkName harness process exited before the server became ready. Exit code: $($process.ExitCode)" + } + + try { + $response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 2 + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { + $isReady = $true + break + } + } + catch { + } + + Start-Sleep -Milliseconds 250 +} + +if (-not $isReady) { + if (-not $process.HasExited) { + Stop-Process -Id $process.Id -Force + } + + throw "$frameworkName harness did not become ready within 30 seconds on $url" +} + +if (-not $NoBrowser) { + Start-Process $url | Out-Null +} + +if ($PassThru) { + $process +} \ No newline at end of file diff --git a/agents/skills/psu-sandbox/SKILL.md b/agents/skills/psu-sandbox/SKILL.md new file mode 100644 index 0000000..5d28dcd --- /dev/null +++ b/agents/skills/psu-sandbox/SKILL.md @@ -0,0 +1,149 @@ +--- +name: sandbox-psu +description: Manages PowerShell Universal in a sandboxed environment, allowing for isolated testing and development of custom components without affecting the global system configuration. This skill sets up a local instance of PowerShell Universal with minimal configuration, ideal for development and experimentation. +--- + +# sandbox-psu + +Spins up a fully isolated PowerShell Universal (PSU) sandbox — either by downloading a pre-built release from the Devolutions CDN or by building and publishing a local .NET project. Each sandbox gets its own data directory, port, log paths, and generated relaunch/teardown scripts, so multiple sandboxes of the same version can run side-by-side without interfering. + +## When to use + +- Quickly test a released version of PSU without a system-wide install +- Run the current branch of a PSU source repo against a clean data directory +- Reproduce bugs in an isolated environment you can throw away +- Develop and test custom PSU components (pair with `custom-component-generate-psu`) + +## Prerequisites + +- PowerShell 7 (`pwsh`) +- Internet access + +## Script + +`.agents/skills/install-sandbox-psu/scripts/Install-PSU.ps1` + +## Parameters + +| Parameter | Default | Description | +|---|---|---| +| `-PSUVersion` | `"latest"` | Version to install from CDN (e.g. `"2025.3.1.0"`). | +| `-SandboxId` | _(random 6-char hex)_ | A name to identify this sandbox. Reuse the same ID to restart against the same data directory and port. | +| `-Empty` | — | _(default parameter set)_ Start with an empty PSU repository — no configuration seeded. | +| `-ConfigurationDirectory` | _(none)_ | Path to a directory whose contents are copied into the PSU repository folder before the server starts. Mutually exclusive with `-GitRepo` and `-Empty`. | +| `-Git` | — | Use git to clone a repository. Use `GitRepo` to specify the URL. Uses the demo repo by default. | +| `-GitRepo` | `"https://github.com/Devolutions/powershell-universal-demo"` | Git repository URL to clone into the PSU repository folder before the server starts. Mutually exclusive with `-ConfigurationDirectory` and `-Empty`. | +| `-Teardown` | — | Stop the running server process and delete the entire sandbox folder. Requires `-SandboxId`. | +| `-List` | — | List all existing sandboxes under `$env:TEMP`, showing URL, SandboxId, source, creation time, and running status. | +| `-Features` | _(none)_ | Optional list of PSU features/plugins to enable. Supported values: `MCP`, `C#`, `YARP`. | +| `-DatabaseType` | `"SQLite"` | Database backend to use. Options: `SQLite`, `SQL` (SQL Server), `PostgreSQL`. | +| `-DatabaseConnectionString` | _(auto for SQLite)_ | Connection string for the selected database. When omitted with SQLite, defaults to a file in the sandbox data directory. | + +## Sandbox layout + +Each sandbox lives at `$env:TEMP\PSUSandbox--\`: + +``` +PSUSandbox-2026.1.3.0-a3f9c2\ + psu\ PSU binaries (extracted zip or dotnet publish output) + data\ + repository\ PSU configuration repository + psu.db SQLite database + logs\ + systemlog.txt + agent\logs\ + log.txt + Dashboard\ UniversalDashboard assets folder + sandbox.json Stored config for reproducible relaunches + Start-Sandbox.ps1 Auto-generated relaunch script (no rebuild) + Remove-Sandbox.ps1 Auto-generated teardown script +``` + +## Environment variables set + +The following are set as process-scoped environment variables before launching the server, so the child process inherits them: + +| Variable | Value | +|---|---| +| `Data__ConnectionString` | `Data Source=\data\psu.db` | +| `Data__RepositoryPath` | `\data\repository` | +| `SystemLogPath` | `\logs\systemlog.txt` | +| `PsuAgentLogPath` | `\agent\logs\log.txt` | +| `Kestrel__Endpoints__HTTP__Url` | `http://*:` | +| `UniversalDashboard__AssetsFolder` | `\Dashboard` | + +Port is auto-selected from the unused range 5001–6000 on first run and stored in `sandbox.json` for all subsequent runs. + +## Usage examples + +### Install and run the latest CDN release (empty sandbox) + +```powershell +pwsh .agents/skills/install-sandbox-psu/scripts/Install-PSU.ps1 +``` + +### Seed from the default demo Git repo + +```powershell +pwsh Install-PSU.ps1 -GitRepo +``` + +### Seed from a custom Git repo + +```powershell +pwsh Install-PSU.ps1 -GitRepo https://github.com/myorg/my-psu-config -SandboxId myconfig +``` + +### Seed from a local configuration directory + +```powershell +pwsh Install-PSU.ps1 -ConfigurationDirectory C:\myconfig -SandboxId myconfig +``` + +### Install a specific version + +```powershell +pwsh Install-PSU.ps1 -PSUVersion 2025.3.1.0 -SandboxId testing +``` + +### List all sandboxes + +```powershell +pwsh Install-PSU.ps1 -List +``` + +### Tear down a sandbox + +```powershell +# Stops the process and deletes the entire folder +pwsh Install-PSU.ps1 -Teardown -SandboxId dev + +# Or run the generated script directly +pwsh "$env:TEMP\PSUSandbox-local-dev\Remove-Sandbox.ps1" +``` + +If the teardown fails, don't force close any processes. Let the user know which locks are preventing cleanup so they can investigate or try again after a reboot. + +## Expected output + +On successful start: + +``` +========================================== + PSU Sandbox Started + Version : 2026.1.3.0 + Source : cdn:2026.1.3.0 + SandboxId : a3f9c2 + URL : http://localhost:5001 + Process ID : 12345 + Root : C:\Users\...\Temp\PSUSandbox-2026.1.3.0-a3f9c2 + Relaunch : ...\Start-Sandbox.ps1 + Teardown : ...\Remove-Sandbox.ps1 +========================================== +``` + +## Notes + +- The CDN zip is cached in `$env:TEMP` and reused across sandbox instances of the same version — only the data directory is per-sandbox. +- `sandbox.json` stores the port and all paths. Re-running `Install-PSU.ps1` with the same `-SandboxId` reuses the existing port and data, making runs reproducible. +- The script is idempotent: running it twice with the same `-SandboxId` starts the server against the existing data without re-downloading or re-extracting. diff --git a/agents/skills/psu-sandbox/scripts/Install-PSU.ps1 b/agents/skills/psu-sandbox/scripts/Install-PSU.ps1 new file mode 100644 index 0000000..706d898 --- /dev/null +++ b/agents/skills/psu-sandbox/scripts/Install-PSU.ps1 @@ -0,0 +1,457 @@ +[CmdletBinding(DefaultParameterSetName = 'Empty')] +param( + # Version to install. Use 'latest' for the current release, or specify + # a version string such as '2025.3.1.0'. + [Parameter(HelpMessage = "PSU version to install, e.g. '2025.3.1.0'. Use 'latest' for the current release.")] + [string]$PSUVersion = "latest", + + [Parameter(HelpMessage = "Optional short identifier for this sandbox. A random 6-character ID is generated when omitted.")] + [string]$SandboxId = "", + + [Parameter(HelpMessage = "Stop all processes and permanently delete the sandbox directory.")] + [switch]$Teardown, + + [Parameter(HelpMessage = "List all existing sandboxes found in the temp directory.")] + [switch]$List, + + [Parameter(ParameterSetName = 'Empty', HelpMessage = "Start with an empty PSU repository, no configuration seeded.")] + [switch]$Empty, + + [Parameter(ParameterSetName = 'FromDirectory', HelpMessage = "Path to a directory whose contents will be copied into the PSU repository folder before the server starts.")] + [string]$ConfigurationDirectory, + + [Parameter(ParameterSetName = "FromGit")] + [Switch]$Git, + + [Parameter(ParameterSetName = 'FromGit', HelpMessage = "Git repository URL to clone into the PSU repository folder before the server starts.")] + [string]$GitRepo = "https://github.com/Devolutions/powershell-universal-demo", + + [Parameter(HelpMessage = "Optional list of PSU features/plugins to enable. Supported values: 'MCP', 'C#', 'YARP'.")] + [string[]]$Features, + + [Parameter(HelpMessage = "Database backend to use. Defaults to 'SQLite'. Other options: 'SQL' (SQL Server) or 'PostgreSQL'.")] + [ValidateSet("SQLite", "SQL", "PostgreSQL")] + [string]$DatabaseType = "SQLite", + + [Parameter(HelpMessage = "Connection string for the selected database. When omitted with SQLite, defaults to a file in the sandbox data directory.")] + [string]$DatabaseConnectionString +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# --------------------------------------------------------------------------- +# Helper: pretty summary box +# --------------------------------------------------------------------------- +function Write-Box { + param([string[]]$Lines, [ConsoleColor]$BorderColor = 'Green') + $width = ($Lines | Measure-Object -Property Length -Maximum).Maximum + 4 + $border = '=' * $width + Write-Host $border -ForegroundColor $BorderColor + foreach ($l in $Lines) { Write-Host " $l" -ForegroundColor White } + Write-Host $border -ForegroundColor $BorderColor +} + +# --------------------------------------------------------------------------- +# Fetch productinfo (latest version + fallback for listing) +# --------------------------------------------------------------------------- +Write-Host "Fetching PSU release info..." -ForegroundColor Cyan +$ProductInfo = Invoke-RestMethod https://devolutions.net/productinfo.json +$LatestVersion = $ProductInfo.PowerShellUniversal.Current.Version + +# --------------------------------------------------------------------------- +# Resolve version label +# --------------------------------------------------------------------------- +if ($PSUVersion -eq "latest") { + $Version = $LatestVersion + Write-Host "Latest version : $Version" -ForegroundColor Cyan +} else { + $Version = $PSUVersion + Write-Host "Target version : $Version" -ForegroundColor Cyan +} + +# --------------------------------------------------------------------------- +# -List: show all existing sandboxes +# --------------------------------------------------------------------------- +if ($List) { + $sandboxDirs = Get-ChildItem -Path $env:TEMP -Directory -Filter "PSUSandbox-*" -ErrorAction SilentlyContinue + if (-not $sandboxDirs) { + Write-Host "No sandboxes found." -ForegroundColor Yellow + } else { + Write-Host "" + Write-Host "Existing sandboxes:" -ForegroundColor Cyan + foreach ($dir in $sandboxDirs) { + $cfgPath = Join-Path $dir.FullName "sandbox.json" + if (Test-Path $cfgPath) { + $cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json + $props = $cfg.PSObject.Properties + $storedPid = if ($props['pid']) { $cfg.pid } else { $null } + $running = if ($storedPid) { + Get-Process -Id $storedPid -ErrorAction SilentlyContinue + } else { + Get-Process -ErrorAction SilentlyContinue | + Where-Object { try { $_.Path -eq $cfg.serverExe } catch { $false } } + } + $status = if ($running) { "RUNNING (PID $($running.Id))" } else { "stopped" } + $sid = if ($props['sandboxId']) { $cfg.sandboxId } else { '(unknown)' } + $created = if ($props['createdAt']) { $cfg.createdAt } else { 'unknown' } + $url = if ($props['url']) { $cfg.url } else { 'unknown' } + $src = if ($props['source']) { $cfg.source } else { 'cdn' } + Write-Host " $($dir.Name)" -ForegroundColor White + Write-Host " URL : $url" + Write-Host " SandboxId: $sid" + Write-Host " Source : $src" + Write-Host " Created : $created" + Write-Host " Status : $status" + } else { + Write-Host " $($dir.Name) (no sandbox.json)" -ForegroundColor DarkGray + } + } + Write-Host "" + } + exit 0 +} + +# --------------------------------------------------------------------------- +# Resolve SandboxId +# --------------------------------------------------------------------------- +if ([string]::IsNullOrWhiteSpace($SandboxId)) { + $SandboxId = [System.Guid]::NewGuid().ToString('N').Substring(0, 6) + Write-Host "Generated SandboxId: $SandboxId" -ForegroundColor Cyan +} else { + Write-Host "Using SandboxId : $SandboxId" -ForegroundColor Cyan +} + +# --------------------------------------------------------------------------- +# Directory layout +# +# $SharedPsuDirectory\ Shared PSU binaries (per version, shared across sandboxes) +# Universal.Server.exe +# ... +# +# $SandboxRoot\ +# data\ PSU application data +# repository\ +# logs\ +# agent\logs\ +# Dashboard\ +# sandbox.json +# Start-Sandbox.ps1 +# Remove-Sandbox.ps1 +# --------------------------------------------------------------------------- +$SandboxRoot = Join-Path $env:TEMP "PSUSandbox-${Version}-${SandboxId}" +$SharedPsuDirectory = Join-Path $env:TEMP "PSUBinaries-${Version}" +$DataDirectory = Join-Path $SandboxRoot "data" +$configFile = Join-Path $SandboxRoot "sandbox.json" + +# --------------------------------------------------------------------------- +# -Teardown +# --------------------------------------------------------------------------- +if ($Teardown) { + if (-not (Test-Path $SandboxRoot)) { + Write-Host "Sandbox not found: $SandboxRoot" -ForegroundColor Yellow + exit 0 + } + + Write-Host "Tearing down sandbox: $SandboxRoot" -ForegroundColor Cyan + + $stopped = 0 + # Try stored PID first for a precise stop + if (Test-Path $configFile) { + $storedCfg = Get-Content $configFile -Raw | ConvertFrom-Json + if ($storedCfg.PSObject.Properties['pid'] -and $storedCfg.pid) { + $byPid = Get-Process -Id $storedCfg.pid -ErrorAction SilentlyContinue + if ($byPid) { + Write-Host " Stopping PID $($byPid.Id) ($($byPid.Name)) [from sandbox.json]..." -ForegroundColor Yellow + Stop-Process -Id $byPid.Id -Force -ErrorAction SilentlyContinue + $stopped++ + } + } + } + # Fallback: scan processes by path for any survivors + $resolvedRoot = (Resolve-Path $SandboxRoot -ErrorAction SilentlyContinue)?.Path ?? $SandboxRoot + Get-Process -ErrorAction SilentlyContinue | Where-Object { + try { $_.Path -and $_.Path.StartsWith($resolvedRoot, [System.StringComparison]::OrdinalIgnoreCase) } catch { $false } + } | ForEach-Object { + Write-Host " Stopping PID $($_.Id) ($($_.Name))..." -ForegroundColor Yellow + Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue + $stopped++ + } + if ($stopped -eq 0) { Write-Host " No running processes found." -ForegroundColor DarkGray } + if ($stopped -gt 0) { Start-Sleep -Seconds 2 } + + Remove-Item -Path $SandboxRoot -Recurse -Force + Write-Host "" + Write-Box @( + "Sandbox $SandboxId torn down.", + "Version : $Version", + "Path : $SandboxRoot" + ) -BorderColor Yellow + exit 0 +} + +# --------------------------------------------------------------------------- +# Create directory structure +# --------------------------------------------------------------------------- +foreach ($d in @( + $SharedPsuDirectory, + $DataDirectory, + (Join-Path $DataDirectory "repository"), + (Join-Path $SandboxRoot "logs"), + (Join-Path $SandboxRoot "agent\logs"), + (Join-Path $SandboxRoot "Dashboard") +)) { + if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } +} +Write-Host "Sandbox root : $SandboxRoot" -ForegroundColor Cyan +Write-Host "Shared bins : $SharedPsuDirectory" -ForegroundColor Cyan + +# --------------------------------------------------------------------------- +# Download PSU binaries from CDN +# --------------------------------------------------------------------------- +$ZipFile = Join-Path $env:TEMP "Devolutions.PowerShellUniversal.win-x64.$Version.zip" + +if (-not (Test-Path $ZipFile)) { + $downloadUrl = "https://cdn.devolutions.net/download/Devolutions.PowerShellUniversal.win-x64.$Version.zip" + Write-Host "Downloading $downloadUrl ..." -ForegroundColor Cyan + Invoke-RestMethod $downloadUrl -OutFile $ZipFile + Write-Host "Download complete." -ForegroundColor Green +} else { + Write-Host "Zip already cached: $ZipFile" -ForegroundColor DarkGray +} + +if (-not (Test-Path (Join-Path $SharedPsuDirectory "Universal.Server.exe")) -and + -not (Test-Path (Join-Path $SharedPsuDirectory "Universal.Server"))) { + Write-Host "Extracting to $SharedPsuDirectory ..." -ForegroundColor Cyan + Expand-Archive -Path $ZipFile -DestinationPath $SharedPsuDirectory -Force + Write-Host "Extraction complete." -ForegroundColor Green +} else { + Write-Host "PSU binaries already extracted: $SharedPsuDirectory" -ForegroundColor DarkGray +} +$sourceLabel = "cdn:$Version" + +# --------------------------------------------------------------------------- +# Locate server executable +# --------------------------------------------------------------------------- +$serverExe = Get-ChildItem -Path $SharedPsuDirectory -Filter "Universal.Server.exe" -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 +if ($null -eq $serverExe) { + $serverExe = Get-ChildItem -Path $SharedPsuDirectory -Filter "Universal.Server" -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 +} +if ($null -eq $serverExe) { throw "Could not find Universal.Server executable under $SharedPsuDirectory" } + +# --------------------------------------------------------------------------- +# Port selection — reuse stored port if sandbox.json exists, else pick free +# --------------------------------------------------------------------------- +if (Test-Path $configFile) { + $storedConfig = Get-Content $configFile -Raw | ConvertFrom-Json + $Port = $storedConfig.port + Write-Host "Reusing stored port : $Port (from sandbox.json)" -ForegroundColor DarkGray +} else { + $usedPorts = [System.Collections.Generic.HashSet[int]]::new() + foreach ($conn in (Get-NetTCPConnection -ErrorAction SilentlyContinue)) { + [void]$usedPorts.Add($conn.LocalPort) + } + $Port = $null + for ($p = 5001; $p -le 6000; $p++) { + if (-not $usedPorts.Contains($p)) { $Port = $p; break } + } + if ($null -eq $Port) { throw "No free port found in range 5001-6000." } + Write-Host "Selected port : $Port" -ForegroundColor Cyan +} + +# --------------------------------------------------------------------------- +# Environment variables +# --------------------------------------------------------------------------- + +if (-not $DatabaseConnectionString -and $DatabaseType -eq "SQLite") +{ + $DatabaseConnectionString = "Data Source=$DataDirectory\psu.db" +} + +$envVars = [ordered]@{ + Data__ConnectionString = $DatabaseConnectionString + Data__RepositoryPath = "$DataDirectory\repository" + SystemLogPath = "$SandboxRoot\logs\systemlog.txt" + PsuAgentLogPath = "$SandboxRoot\agent\logs\log.txt" + 'Kestrel__Endpoints__HTTP__Url' = "http://*:$Port" + UniversalDashboard__AssetsFolder = "$SandboxRoot\Dashboard" +} + +$Plugins = @($DatabaseType) + +if ("MCP" -in $Features) { + $Plugins += "PowerShellUniversal.Plugin.MCP" +} + +if ("C#" -in $Features) { + $Plugins += "PowerShellUniversal.Language.CSharp" +} + +if ("YARP" -in $Features) { + $Plugins += "PowerShellUniversal.Plugin.YARP" +} + +$i = 0 +foreach ($plugin in $Plugins) { + $envVars["Plugins__$i"] = $plugin + $i++ +} + +foreach ($key in $envVars.Keys) { + [System.Environment]::SetEnvironmentVariable($key, $envVars[$key], 'Process') +} + +# --------------------------------------------------------------------------- +# Save/update sandbox.json +# --------------------------------------------------------------------------- +$config = [ordered]@{ + version = $Version + sandboxId = $SandboxId + source = $sourceLabel + sandboxRoot = $SandboxRoot + port = $Port + url = "http://localhost:$Port" + serverExe = $serverExe.FullName + pid = $null + envVars = $envVars + createdAt = if (Test-Path $configFile) { (Get-Content $configFile -Raw | ConvertFrom-Json).createdAt } else { (Get-Date -Format "o") } +} +$config | ConvertTo-Json -Depth 5 | Set-Content -Path $configFile -Encoding UTF8 + +# --------------------------------------------------------------------------- +# Write Start-Sandbox.ps1 +# --------------------------------------------------------------------------- +$startScript = Join-Path $SandboxRoot "Start-Sandbox.ps1" +$removeScript = Join-Path $SandboxRoot "Remove-Sandbox.ps1" +$envLines = $envVars.GetEnumerator() | ForEach-Object { "`$env:$($_.Key) = '$($_.Value)'" } + +@" +# Auto-generated — re-run to restart this PSU sandbox without rebuilding. +# Version: $Version SandboxId: $SandboxId Source: $sourceLabel +Set-StrictMode -Version Latest +`$ErrorActionPreference = 'Stop' + +$($envLines -join "`n") + +Push-Location '$($serverExe.DirectoryName)' +try { + `$proc = Start-Process -FilePath '$($serverExe.FullName)' -PassThru -NoNewWindow -RedirectStandardOutput '$SandboxRoot\logs\server-stdout.txt' -RedirectStandardError '$SandboxRoot\logs\server-stderr.txt' + # Persist the new PID so teardown/list can target it directly + `$cfg = Get-Content '$configFile' -Raw | ConvertFrom-Json + `$cfg.pid = `$proc.Id + `$cfg | ConvertTo-Json -Depth 5 | Set-Content -Path '$configFile' -Encoding UTF8 + Write-Host "" + Write-Host " Sandbox root : $SandboxRoot" + Write-Host " URL : http://localhost:$Port" + Write-Host " Process ID : `$(`$proc.Id)" + Write-Host " SandboxId : $SandboxId" + Write-Host "" + `$proc.WaitForExit() +} finally { + Pop-Location +} +"@ | Set-Content -Path $startScript -Encoding UTF8 + +# --------------------------------------------------------------------------- +# Write Remove-Sandbox.ps1 +# --------------------------------------------------------------------------- +@" +# Auto-generated — stops and permanently deletes this sandbox. +# Version: $Version SandboxId: $SandboxId +`$ErrorActionPreference = 'Stop' + +Write-Host "Stopping processes for sandbox $SandboxId..." -ForegroundColor Cyan +`$resolvedRoot = (Resolve-Path '$SandboxRoot' -ErrorAction SilentlyContinue)?.Path ?? '$SandboxRoot' +Get-Process -ErrorAction SilentlyContinue | Where-Object { + try { `$_.Path -and `$_.Path.StartsWith(`$resolvedRoot, [System.StringComparison]::OrdinalIgnoreCase) } catch { `$false } +} | ForEach-Object { + Write-Host " Stopping PID `$(`$_.Id)..." -ForegroundColor Yellow + Stop-Process -Id `$_.Id -Force -ErrorAction SilentlyContinue +} +Start-Sleep -Seconds 2 +Write-Host "Removing $SandboxRoot ..." -ForegroundColor Cyan +Remove-Item -Path '$SandboxRoot' -Recurse -Force +Write-Host "Done." -ForegroundColor Green +"@ | Set-Content -Path $removeScript -Encoding UTF8 + +Write-Host "Start script : $startScript" -ForegroundColor DarkGray +Write-Host "Remove script : $removeScript" -ForegroundColor DarkGray + +# --------------------------------------------------------------------------- +# Seed repository from ConfigurationDirectory or GitRepo +# --------------------------------------------------------------------------- +if (-not [string]::IsNullOrWhiteSpace($ConfigurationDirectory)) { + if (-not (Test-Path $ConfigurationDirectory)) { + throw "ConfigurationDirectory not found: $ConfigurationDirectory" + } + $repoDirectory = Join-Path $DataDirectory "repository" + Write-Host "Seeding repository from: $ConfigurationDirectory" -ForegroundColor Cyan + Copy-Item -Path (Join-Path $ConfigurationDirectory "*") -Destination $repoDirectory -Recurse -Force + Write-Host "Repository seeded." -ForegroundColor Green +} elseif ($PSCmdlet.ParameterSetName -eq 'FromGit') { + $repoDirectory = Join-Path $DataDirectory "repository" + Write-Host "Cloning repository from: $GitRepo" -ForegroundColor Cyan + git clone $GitRepo $repoDirectory + Write-Host "Repository cloned." -ForegroundColor Green +} + +# --------------------------------------------------------------------------- +# Launch PSU server +# --------------------------------------------------------------------------- +Push-Location $serverExe.DirectoryName +try { + $proc = Start-Process -FilePath $serverExe.FullName -PassThru -NoNewWindow -RedirectStandardOutput "$SandboxRoot\logs\server-stdout.txt" -RedirectStandardError "$SandboxRoot\logs\server-stderr.txt" + + # Persist the PID so teardown/list can target it directly + $savedConfig = Get-Content $configFile -Raw | ConvertFrom-Json + $savedConfig.pid = $proc.Id + $savedConfig | ConvertTo-Json -Depth 5 | Set-Content -Path $configFile -Encoding UTF8 + + Write-Host "" + $summaryLines = @( + "PSU Sandbox Started", + " Version : $Version", + " Source : $sourceLabel", + " SandboxId : $SandboxId", + " URL : http://localhost:$Port", + " Process ID : $($proc.Id)", + " Root : $SandboxRoot", + " Relaunch : $startScript", + " Teardown : $removeScript", + " Features : $($Features -join ', ')", + " Database : $DatabaseType", + " Repo source : $($ConfigurationDirectory ?? $GitRepo ?? '(none)')", + " Default credentials: admin / admin" + ) + + try { + $Status = Invoke-RestMethod -Uri "http://localhost:$Port/api/v1/alive" -ErrorAction SilentlyContinue -SkipHttpErrorCheck + } catch { + $Status = $null + } + + while($null -eq $Status -or $Status.loading) { + Write-Host "Waiting for PSU to start..." -ForegroundColor Cyan + Start-Sleep -Seconds 2 + try { + $Status = Invoke-RestMethod -Uri "http://localhost:$Port/api/v1/alive" -ErrorAction SilentlyContinue -SkipHttpErrorCheck + } catch { + $Status = $null + } + } + + $null = Invoke-RestMethod -Uri "http://localhost:$Port/api/v1/first-run" -Method Post -Body (@{ userName = "admin"; password = "admin" } | ConvertTo-Json) -ContentType "application/json" + + Start-Process "http://localhost:$Port" + + Write-Box $summaryLines + Write-Host "" + Write-Host "To stop : Stop-Process -Id $($proc.Id)" -ForegroundColor DarkGray + Write-Host "To remove: pwsh '$removeScript'" -ForegroundColor DarkGray + Write-Host "To list : pwsh '$PSCommandPath' -List" -ForegroundColor DarkGray + Write-Host "" +} finally { + Pop-Location +} \ No newline at end of file