From f737b1d11d794e9daf69f5838595f506925892af Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 7 Feb 2026 20:03:27 +0200 Subject: [PATCH] feat(globalStateStore): implement global state management with persistence - refactor state structure to include app, layout, navigation, owner, program, session, and user - add slices for managing program, session, owner, user, layout, navigation, and app states - create context provider for global state with automatic fetching and throttling - implement persistence using IndexedDB with localStorage fallback - add comprehensive README documentation for usage and API --- .storybook/preview.ts | 13 +- .storybook/previewDecorator.tsx | 40 +- README.md | 381 +++++-- mcp-server.json | 86 ++ mcp/README.md | 102 ++ mcp/server.js | 953 ++++++++++++++++++ package.json | 17 +- pnpm-lock.yaml | 493 +++++++++ src/Boxer/hooks/useBoxerOptions.tsx | 12 +- src/Former/Former.tsx | 2 +- src/Former/Former.types.ts | 6 +- src/Former/FormerLayoutBottom.tsx | 9 +- src/Former/FormerLayoutTop.tsx | 9 +- .../GlobalStateStore.stories.tsx | 411 ++++++++ src/GlobalStateStore/GlobalStateStore.ts | 423 ++++---- .../GlobalStateStore.types.ts | 206 +++- .../GlobalStateStore.utils.ts | 154 ++- .../GlobalStateStoreWrapper.tsx | 107 ++ src/GlobalStateStore/README.md | 105 ++ src/GlobalStateStore/index.ts | 17 +- src/Gridler/stories/Examples.goapi.tsx | 39 +- src/lib.ts | 1 + 22 files changed, 3098 insertions(+), 488 deletions(-) create mode 100644 mcp-server.json create mode 100644 mcp/README.md create mode 100755 mcp/server.js create mode 100644 src/GlobalStateStore/GlobalStateStore.stories.tsx create mode 100644 src/GlobalStateStore/README.md diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 3250370..29c8b99 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,18 +1,19 @@ -import type { Preview } from '@storybook/react-vite' +import type { Preview } from '@storybook/react-vite'; + import { PreviewDecorator } from './previewDecorator'; const preview: Preview = { + decorators: [PreviewDecorator], parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { - color: /(background|color)$/i, - date: /Date$/i, + color: /(background|color)$/i, + date: /Date$/i, }, }, + layout: 'fullscreen', }, - decorators: [ - PreviewDecorator, - ], }; export default preview; \ No newline at end of file diff --git a/.storybook/previewDecorator.tsx b/.storybook/previewDecorator.tsx index 36e0979..671d85b 100644 --- a/.storybook/previewDecorator.tsx +++ b/.storybook/previewDecorator.tsx @@ -1,16 +1,40 @@ -import { MantineProvider } from '@mantine/core'; -import { ModalsProvider } from '@mantine/modals'; import '@mantine/core/styles.css'; -export function PreviewDecorator(Story: any, { parameters }: any) { - console.log('Rendering decorator', parameters); +import type { Decorator } from '@storybook/react-vite'; + +import { MantineProvider } from '@mantine/core'; +import { ModalsProvider } from '@mantine/modals'; + +import { GlobalStateStoreProvider } from '../src/GlobalStateStore'; + +export const PreviewDecorator: Decorator = (Story, context) => { + const { parameters } = context; + + // Allow stories to opt-out of GlobalStateStore provider + const useGlobalStore = parameters.globalStore !== false; + + // Use 100% for fullscreen layout, 100vh otherwise + const isFullscreen = parameters.layout === 'fullscreen'; + const containerStyle = { + height: isFullscreen ? '100%' : '100vh', + width: isFullscreen ? '100%' : '100vw', + }; + return ( -
- -
+ {useGlobalStore ? ( + +
+ +
+
+ ) : ( +
+ +
+ )}
); -} +}; diff --git a/README.md b/README.md index 07da8a7..2482229 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,38 @@ Oranguru is a comprehensive component library that extends Mantine's component e Currently featuring advanced menu components, Oranguru is designed to grow into a full suite of enhanced Mantine components that offer more flexibility and power than their standard counterparts. -## Features +## Components -### Current Components -- **Enhanced Context Menus**: Better menu positioning and visibility control -- **Custom Rendering**: Support for custom menu item renderers and complete menu rendering -- **Async Actions**: Built-in support for async menu item actions with loading states +### MantineBetterMenu + +Enhanced context menus with better positioning and visibility control + +### Gridler + +Powerful data grid component with sorting, filtering, and pagination + +### Former + +Form component with React Hook Form integration and validation + +### FormerControllers + +Pre-built form input controls for use with Former + +### Boxer + +Advanced combobox/select with virtualization and server-side data support + +### ErrorBoundary + +React error boundary components for graceful error handling + +### GlobalStateStore + +Zustand-based global state management with automatic persistence + +## Core Features -### Core Features - **State Management**: Zustand-based store for component state management - **TypeScript Support**: Full TypeScript definitions included - **Portal-based Rendering**: Proper z-index handling through React portals @@ -37,133 +61,266 @@ npm install react@">= 19.0.0" zustand@">= 5.0.0" @mantine/core@"^8.3.1" @mantine ## Usage -### Basic Setup +### MantineBetterMenu ```tsx -import { MantineBetterMenusProvider } from '@warkypublic/oranguru'; -import { MantineProvider } from '@mantine/core'; +import { MantineBetterMenusProvider, useMantineBetterMenus } from '@warkypublic/oranguru'; -function App() { - return ( - - - {/* Your app content */} - - - ); -} -``` +// Wrap app with provider + + + -### Using the Menu Hook - -```tsx -import { useMantineBetterMenus } from '@warkypublic/oranguru'; - -function MyComponent() { - const { show, hide } = useMantineBetterMenus(); - - const handleContextMenu = (e: React.MouseEvent) => { - e.preventDefault(); - show('my-menu', { - x: e.clientX, - y: e.clientY, - items: [ - { - label: 'Edit', - onClick: () => console.log('Edit clicked') - }, - { - label: 'Delete', - onClick: () => console.log('Delete clicked') - }, - { - isDivider: true - }, - { - label: 'Async Action', - onClickAsync: async () => { - await new Promise(resolve => setTimeout(resolve, 2000)); - console.log('Async action completed'); - } - } - ] - }); - }; - - return ( -
- Right-click me for a context menu -
- ); -} -``` - -### Custom Menu Items - -```tsx -const customMenuItem = { - renderer: ({ loading }: any) => ( -
- {loading ? 'Loading...' : 'Custom Item'} -
- ) -}; - -show('custom-menu', { +// Use in components +const { show, hide } = useMantineBetterMenus(); +show('menu-id', { x: e.clientX, y: e.clientY, - items: [customMenuItem] + items: [ + { label: 'Edit', onClick: () => {} }, + { isDivider: true }, + { label: 'Async', onClickAsync: async () => {} } + ] }); ``` +### Gridler + +```tsx +import { Gridler } from '@warkypublic/oranguru'; + +// Local data + + + + +// API data + + + + +// With inline editing form + + + { + setFormProps({ opened: true, request, values: data }); + }} + /> + + + setFormProps({ opened: false })} +> + + + + +// Columns definition +const columns = [ + { id: 'name', title: 'Name', width: 200 }, + { id: 'email', title: 'Email', width: 250 } +]; +``` + +### Former + +```tsx +import { Former, FormerDialog } from '@warkypublic/oranguru'; + +const formRef = useRef(null); + + { /* save logic */ }} + primeData={{ name: '', email: '' }} + wrapper={FormerDialog} +> + {/* Form content */} + + +// Methods: formRef.current.show(), .save(), .reset() +``` + +### FormerControllers + +```tsx +import { + TextInputCtrl, + PasswordInputCtrl, + NativeSelectCtrl, + TextAreaCtrl, + SwitchCtrl, + ButtonCtrl +} from '@warkypublic/oranguru'; + + + + + + + Save + +``` + +### Boxer + +```tsx +import { Boxer } from '@warkypublic/oranguru'; + +// Local data + + +// Server-side data + ({ + data: [...], + total: 100 + })} + value={value} + onChange={setValue} +/> + +// Multi-select + +``` + +### ErrorBoundary + +```tsx +import { ReactErrorBoundary, ReactBasicErrorBoundary } from '@warkypublic/oranguru'; + +// Full-featured error boundary + {}} +> + + + +// Basic error boundary + + + +``` + +### GlobalStateStore + +```tsx +import { + GlobalStateStoreProvider, + useGlobalStateStore, + GlobalStateStore +} from '@warkypublic/oranguru'; + +// Wrap app + + + + +// Use in components +const { program, session, user, layout } = useGlobalStateStore(); +const { refetch } = useGlobalStateStoreContext(); + +// Outside React +GlobalStateStore.getState().setAuthToken('token'); +const apiURL = GlobalStateStore.getState().session.apiURL; +``` + ## API Reference -### MantineBetterMenusProvider +**MantineBetterMenu** -The main provider component that wraps your application. +- Provider: `MantineBetterMenusProvider` +- Hook: `useMantineBetterMenus()` returns `{ show, hide, menus, setInstanceState }` +- Key Props: `items[]`, `x`, `y`, `visible`, `menuProps`, `renderer` -**Props:** -- `providerID?`: Optional unique identifier for the provider instance +**Gridler** -### useMantineBetterMenus +- Main Component: `Gridler` +- Adaptors: `LocalDataAdaptor`, `APIAdaptorForGoLangv2`, `FormAdaptor` +- Store Hook: `useGridlerStore()` +- Key Props: `uniqueid`, `columns[]`, `data` -Hook to access menu functionality. +**Former** -**Returns:** -- `show(id: string, options?: Partial)`: Show a menu -- `hide(id: string)`: Hide a menu -- `menus`: Array of current menu instances -- `setInstanceState`: Update specific menu instance properties +- Main Component: `Former` +- Wrappers: `FormerDialog`, `FormerModel`, `FormerPopover` +- Ref Methods: `show()`, `close()`, `save()`, `reset()`, `validate()` +- Key Props: `primeData`, `onSave`, `wrapper` -### MantineBetterMenuInstance +**FormerControllers** -Interface for menu instances: +- Controls: `TextInputCtrl`, `PasswordInputCtrl`, `TextAreaCtrl`, `NativeSelectCtrl`, `SwitchCtrl`, `ButtonCtrl`, `IconButtonCtrl` +- Common Props: `name` (required), `label`, `disabled` -```typescript -interface MantineBetterMenuInstance { - id: string; - items?: Array; - menuProps?: MenuProps; - renderer?: ReactNode; - visible: boolean; - x: number; - y: number; +**Boxer** + +- Provider: `BoxerProvider` +- Store Hook: `useBoxerStore()` +- Data Sources: `local`, `server` +- Key Props: `data`, `dataSource`, `onAPICall`, `multiSelect`, `searchable`, `clearable` + +**ErrorBoundary** + +- Components: `ReactErrorBoundary`, `ReactBasicErrorBoundary` +- Key Props: `namespace`, `reportAPI`, `onResetClick`, `onRetryClick` + +**GlobalStateStore** + +- Provider: `GlobalStateStoreProvider` +- Hook: `useGlobalStateStore()` returns `{ program, session, owner, user, layout, navigation, app }` +- Store Methods: `setAuthToken()`, `setApiURL()`, `fetchData()`, `login()`, `logout()` +- Key Props: `apiURL`, `autoFetch`, `fetchOnMount`, `throttleMs` + +## MCP Server + +Oranguru includes a Model Context Protocol (MCP) server for AI-assisted development. + +**Configuration:** + +Add to `~/.claude/mcp_settings.json`: + +```json +{ + "mcpServers": { + "oranguru-docs": { + "command": "npx", + "args": ["-y", "@warkypublic/oranguru", "mcp"] + } + } } ``` -### MantineBetterMenuInstanceItem +**Tools:** -Interface for menu items: +- `list_components` - List all components +- `get_component_docs` - Get component documentation +- `get_component_example` - Get code examples -```typescript -interface MantineBetterMenuInstanceItem extends Partial { - isDivider?: boolean; - label?: string; - onClick?: (e?: React.MouseEvent) => void; - onClickAsync?: () => Promise; - renderer?: ((props: MantineBetterMenuInstanceItem & Record) => ReactNode) | ReactNode; -} -``` +**Resources:** + +- `oranguru://docs/readme` - Full documentation +- `oranguru://docs/components` - Component list + +See `mcp/README.md` for details. ## Development @@ -174,6 +331,7 @@ interface MantineBetterMenuInstanceItem extends Partial { - `pnpm lint`: Run ESLint - `pnpm typecheck`: Run TypeScript type checking - `pnpm clean`: Clean node_modules and dist folders +- `pnpm mcp`: Run MCP server ### Building @@ -189,9 +347,10 @@ See [LICENSE](LICENSE) file for details. ## About the Name -Oranguru is named after the Orangutan Pokémon (オランガ Oranga), a Normal/Psychic-type Pokémon introduced in Generation VII. Known as the "Sage Pokémon," Oranguru is characterized by its wisdom, intelligence, and ability to use tools strategically. +Oranguru is named after the Orangutan Pokémon (オランガ Oranga), a Normal/Psychic-type Pokémon introduced in Generation VII. Known as the "Sage Pokémon," Oranguru is characterized by its wisdom, intelligence, and ability to use tools strategically. In the Pokémon world, Oranguru is known for: + - Its exceptional intelligence and strategic thinking - Living deep in forests and rarely showing itself to humans - Using its psychic powers to control other Pokémon with its fan @@ -201,4 +360,4 @@ Just as Oranguru the Pokémon enhances and controls its environment with wisdom ## Author -**Warky Devs** \ No newline at end of file +Warky Devs diff --git a/mcp-server.json b/mcp-server.json new file mode 100644 index 0000000..ca37d4e --- /dev/null +++ b/mcp-server.json @@ -0,0 +1,86 @@ +{ + "name": "@warkypublic/oranguru-mcp", + "version": "0.0.31", + "description": "MCP server for Oranguru component library documentation and code generation", + "mcpServers": { + "oranguru-docs": { + "command": "node", + "args": ["mcp/server.js"], + "env": {} + } + }, + "tools": [ + { + "name": "get_component_docs", + "description": "Get documentation for a specific Oranguru component", + "inputSchema": { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "MantineBetterMenu", + "Gridler", + "Former", + "FormerControllers", + "Boxer", + "ErrorBoundary", + "GlobalStateStore" + ], + "description": "The component name" + } + }, + "required": ["component"] + } + }, + { + "name": "get_component_example", + "description": "Generate code example for a specific Oranguru component", + "inputSchema": { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "MantineBetterMenu", + "Gridler", + "Former", + "FormerControllers", + "Boxer", + "ErrorBoundary", + "GlobalStateStore" + ], + "description": "The component name" + }, + "variant": { + "type": "string", + "description": "Example variant (e.g., 'basic', 'advanced', 'with-api')" + } + }, + "required": ["component"] + } + }, + { + "name": "list_components", + "description": "List all available Oranguru components", + "inputSchema": { + "type": "object", + "properties": {} + } + } + ], + "resources": [ + { + "uri": "oranguru://docs/readme", + "name": "Oranguru Documentation", + "description": "Main documentation for the Oranguru library", + "mimeType": "text/markdown" + }, + { + "uri": "oranguru://docs/components", + "name": "Component List", + "description": "List of all available components", + "mimeType": "application/json" + } + ] +} diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..1549536 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,102 @@ +# Oranguru MCP Server + +Model Context Protocol server for Oranguru component library documentation and code generation. + +## Installation + +```bash +npm install @warkypublic/oranguru +``` + +## Configuration + +Add to your Claude Code MCP settings (`~/.claude/mcp_settings.json`): + +```json +{ + "mcpServers": { + "oranguru-docs": { + "command": "node", + "args": ["./node_modules/@warkypublic/oranguru/mcp/server.js"] + } + } +} +``` + +Or use npx: + +```json +{ + "mcpServers": { + "oranguru-docs": { + "command": "npx", + "args": ["-y", "@warkypublic/oranguru", "mcp"] + } + } +} +``` + +## Available Tools + +### `list_components` +List all available Oranguru components + +**Returns:** JSON array of components with name, description, and exports + +### `get_component_docs` +Get detailed documentation for a specific component + +**Parameters:** +- `component` (required): Component name (MantineBetterMenu, Gridler, Former, etc.) + +**Returns:** JSON object with component details, exports, and usage information + +### `get_component_example` +Get code examples for a specific component + +**Parameters:** +- `component` (required): Component name +- `variant` (optional): Example variant ('basic', 'local', 'server', etc.) + +**Returns:** Code example string + +## Available Resources + +### `oranguru://docs/readme` +Full README documentation + +**MIME Type:** text/markdown + +### `oranguru://docs/components` +Component list in JSON format + +**MIME Type:** application/json + +## Components + +- **MantineBetterMenu** - Enhanced context menus +- **Gridler** - Data grid component +- **Former** - Form component with React Hook Form +- **FormerControllers** - Form input controls +- **Boxer** - Advanced combobox/select +- **ErrorBoundary** - Error boundary components +- **GlobalStateStore** - Global state management + +## Usage in Claude Code + +Once configured, you can ask Claude Code: + +- "Show me examples of the Gridler component" +- "Get documentation for the Former component" +- "List all Oranguru components" +- "Generate a code example for Boxer with server-side data" + +## Running Locally + +```bash +npm run mcp +``` + +## License + +See main package LICENSE file diff --git a/mcp/server.js b/mcp/server.js new file mode 100755 index 0000000..2bb57e0 --- /dev/null +++ b/mcp/server.js @@ -0,0 +1,953 @@ +#!/usr/bin/env node + +/** + * Oranguru MCP Server + * Provides documentation and code generation for Oranguru components + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Component documentation data +const COMPONENTS = { + Boxer: { + component: 'Boxer', + description: 'Advanced combobox/select with virtualization and server-side data support', + examples: { + basic: `import { Boxer } from '@warkypublic/oranguru'; +import { useState } from 'react'; + +const sampleData = [ + { label: 'Apple', value: 'apple' }, + { label: 'Banana', value: 'banana' }, + { label: 'Cherry', value: 'cherry' } +]; + +function MyComponent() { + const [value, setValue] = useState(null); + + return ( + + ); +}`, + multiSelect: `import { Boxer } from '@warkypublic/oranguru'; +import { useState } from 'react'; + +function MultiSelectExample() { + const [values, setValues] = useState([]); + + return ( + + ); +}`, + server: `import { Boxer } from '@warkypublic/oranguru'; + +function ServerSideExample() { + const [value, setValue] = useState(null); + + const handleAPICall = async ({ page, pageSize, search }) => { + const response = await fetch(\`/api/items?page=\${page}&size=\${pageSize}&search=\${search}\`); + const result = await response.json(); + + return { + data: result.items, + total: result.total + }; + }; + + return ( + + ); +}` + }, + exports: ['Boxer', 'BoxerProvider', 'useBoxerStore'], + hook: 'useBoxerStore()', + name: 'Boxer', + provider: 'BoxerProvider' + }, + ErrorBoundary: { + components: ['ReactErrorBoundary', 'ReactBasicErrorBoundary'], + description: 'React error boundary components for graceful error handling', + examples: { + basic: `import { ReactBasicErrorBoundary } from '@warkypublic/oranguru'; + +function App() { + return ( + + + + ); +}`, + full: `import { ReactErrorBoundary } from '@warkypublic/oranguru'; + +function App() { + const handleReportError = () => { + console.log('Report error to support'); + }; + + const handleReset = () => { + console.log('Reset application state'); + window.location.reload(); + }; + + const handleRetry = () => { + console.log('Retry failed operation'); + }; + + return ( + + + + ); +}`, + nested: `import { ReactErrorBoundary } from '@warkypublic/oranguru'; + +// Multiple error boundaries for granular error handling +function App() { + return ( + +
+ + + + + + + + ); +}`, + globalConfig: `import { SetErrorBoundaryOptions } from '@warkypublic/oranguru'; + +// Configure error boundary globally +SetErrorBoundaryOptions({ + disabled: false, // Set to true to pass through errors (dev mode) + onError: (error, errorInfo) => { + console.error('Global error handler:', error); + // Send to analytics service + analytics.trackError(error, errorInfo); + } +});` + }, + exports: ['ReactErrorBoundary', 'ReactBasicErrorBoundary', 'SetErrorBoundaryOptions', 'GetErrorBoundaryOptions'], + name: 'ErrorBoundary' + }, + Former: { + component: 'Former', + description: 'Form component with React Hook Form integration and validation', + examples: { + basic: `import { Former } from '@warkypublic/oranguru'; +import { useRef } from 'react'; +import { Controller } from 'react-hook-form'; + +function BasicForm() { + const formRef = useRef(null); + + return ( + { + console.log('Saving:', data); + await fetch('/api/save', { + method: 'POST', + body: JSON.stringify(data) + }); + }} + > + ( + + )} + rules={{ required: 'Name is required' }} + /> + ( + + )} + rules={{ required: 'Email is required' }} + /> + + + ); +}`, + withWrapper: `import { Former, FormerModel } from '@warkypublic/oranguru'; +import { useState } from 'react'; + +function ModalForm() { + const [opened, setOpened] = useState(false); + + return ( + <> + + + setOpened(false)} + > + } + /> + + + ); +}`, + withAPI: `import { Former, FormerRestHeadSpecAPI } from '@warkypublic/oranguru'; + +function APIForm() { + return ( + + {/* Form fields */} + + ); +}`, + customLayout: `import { Former } from '@warkypublic/oranguru'; + +function CustomLayoutForm() { + return ( + + {/* Form fields */} + + ); +}`, + refMethods: `import { Former } from '@warkypublic/oranguru'; +import { useRef } from 'react'; + +function FormWithRef() { + const formRef = useRef(null); + + const handleValidate = async () => { + const isValid = await formRef.current?.validate(); + console.log('Form valid:', isValid); + }; + + const handleSave = async () => { + const result = await formRef.current?.save(); + console.log('Save result:', result); + }; + + const handleReset = () => { + formRef.current?.reset(); + }; + + return ( +
+ + {/* Form fields */} + + + + +
+ ); +}` + }, + exports: ['Former', 'FormerDialog', 'FormerModel', 'FormerPopover', 'FormerRestHeadSpecAPI'], + name: 'Former', + wrappers: ['FormerDialog', 'FormerModel', 'FormerPopover'] + }, + FormerControllers: { + controls: ['TextInputCtrl', 'PasswordInputCtrl', 'NativeSelectCtrl', 'TextAreaCtrl', 'SwitchCtrl', 'ButtonCtrl', 'IconButtonCtrl'], + description: 'Pre-built form input controls for use with Former', + examples: { + basic: `import { TextInputCtrl, PasswordInputCtrl, NativeSelectCtrl, ButtonCtrl } from '@warkypublic/oranguru'; + + + + + + Save +` + }, + exports: ['TextInputCtrl', 'PasswordInputCtrl', 'NativeSelectCtrl', 'TextAreaCtrl', 'SwitchCtrl', 'ButtonCtrl', 'IconButtonCtrl'], + name: 'FormerControllers' + }, + GlobalStateStore: { + description: 'Zustand-based global state management with automatic persistence', + examples: { + basic: `import { useGlobalStateStore } from '@warkypublic/oranguru'; + +function MyComponent() { + const state = useGlobalStateStore(); + + return ( +
+

{state.program.name}

+

User: {state.user.username}

+

Email: {state.user.email}

+

Connected: {state.session.connected ? 'Yes' : 'No'}

+
+ ); +}`, + provider: `import { GlobalStateStoreProvider } from '@warkypublic/oranguru'; + +function App() { + return ( + + + + ); +}`, + stateUpdates: `import { useGlobalStateStore } from '@warkypublic/oranguru'; + +function StateControls() { + const state = useGlobalStateStore(); + + const handleUpdateProgram = () => { + state.setProgram({ + name: 'My App', + slug: 'my-app', + description: 'A great application' + }); + }; + + const handleUpdateUser = () => { + state.setUser({ + username: 'john_doe', + email: 'john@example.com', + fullNames: 'John Doe' + }); + }; + + const handleToggleTheme = () => { + state.setUser({ + theme: { + ...state.user.theme, + darkMode: !state.user.theme?.darkMode + } + }); + }; + + return ( +
+ + + +
+ ); +}`, + layout: `import { useGlobalStateStore } from '@warkypublic/oranguru'; + +function LayoutControls() { + const state = useGlobalStateStore(); + + return ( +
+ + + +
+ ); +}`, + outsideReact: `import { GlobalStateStore } from '@warkypublic/oranguru'; + +// Access state outside React components +const currentState = GlobalStateStore.getState(); +console.log('API URL:', currentState.session.apiURL); + +// Update state outside React +GlobalStateStore.getState().setAuthToken('new-token'); +GlobalStateStore.getState().setApiURL('https://new-api.com'); + +// Subscribe to changes +const unsubscribe = GlobalStateStore.subscribe( + (state) => state.session.connected, + (connected) => console.log('Connected:', connected) +);` + }, + exports: ['GlobalStateStore', 'GlobalStateStoreProvider', 'useGlobalStateStore', 'useGlobalStateStoreContext'], + hook: 'useGlobalStateStore()', + name: 'GlobalStateStore', + provider: 'GlobalStateStoreProvider', + store: 'GlobalStateStore' + }, + Gridler: { + adaptors: ['LocalDataAdaptor', 'APIAdaptorForGoLangv2', 'FormAdaptor'], + component: 'Gridler', + description: 'Powerful data grid component with sorting, filtering, and pagination', + examples: { + basic: `import { Gridler } from '@warkypublic/oranguru'; + +const columns = [ + { id: 'name', title: 'Name', width: 200 }, + { id: 'email', title: 'Email', width: 250 }, + { id: 'role', title: 'Role', width: 150 } +]; + +const data = [ + { name: 'John Doe', email: 'john@example.com', role: 'Admin' }, + { name: 'Jane Smith', email: 'jane@example.com', role: 'User' } +]; + +function GridExample() { + return ( + + + + ); +}`, + customCell: `// Custom cell rendering with icons +const columns = [ + { + id: 'status', + title: 'Status', + width: 100, + Cell: (row) => { + const icon = row?.active ? '✅' : '❌'; + return { + data: \`\${icon} \${row?.status}\`, + displayData: \`\${icon} \${row?.status}\` + }; + } + }, + { id: 'name', title: 'Name', width: 200 } +];`, + api: `import { Gridler, GlidlerAPIAdaptorForGoLangv2 } from '@warkypublic/oranguru'; +import { useRef } from 'react'; + +function APIGridExample() { + const gridRef = useRef(null); + + const columns = [ + { id: 'id', title: 'ID', width: 100 }, + { id: 'name', title: 'Name', width: 200 }, + { id: 'status', title: 'Status', width: 150 } + ]; + + return ( + console.log('Selected:', values)} + > + + + ); +}`, + withForm: `// Gridler with Former integration for inline editing +import { Gridler, GlidlerAPIAdaptorForGoLangv2 } from '@warkypublic/oranguru'; +import { FormerDialog } from '@warkypublic/oranguru'; +import { TextInputCtrl, NativeSelectCtrl } from '@warkypublic/oranguru'; +import { useState, useRef } from 'react'; + +function EditableGrid() { + const gridRef = useRef(null); + const [formProps, setFormProps] = useState({ + opened: false, + request: null, + values: null, + onClose: () => setFormProps(prev => ({ + ...prev, + opened: false, + request: null, + values: null + })), + onChange: (request, data) => { + gridRef.current?.refresh({ value: data }); + } + }); + + const columns = [ + { id: 'id', title: 'ID', width: 100 }, + { id: 'name', title: 'Name', width: 200 }, + { id: 'type', title: 'Type', width: 150 } + ]; + + return ( + <> + + + { + setFormProps(prev => ({ + ...prev, + opened: true, + request: request, + values: data + })); + }} + /> + + + + + + + + + ); +}`, + refMethods: `// Using Gridler ref methods for programmatic control +import { Gridler } from '@warkypublic/oranguru'; +import { useRef } from 'react'; + +function GridWithControls() { + const gridRef = useRef(null); + + return ( + <> + + + + +
+ + + + +
+ + ); +}`, + sections: `// Gridler with custom side sections +import { Gridler } from '@warkypublic/oranguru'; +import { useState } from 'react'; + +function GridWithSections() { + const [sections, setSections] = useState({ + top:
Top Bar
, + bottom:
Bottom Bar
, + left:
L
, + right:
R
+ }); + + return ( + + + + ); +}` + }, + exports: ['Gridler', 'GlidlerLocalDataAdaptor', 'GlidlerAPIAdaptorForGoLangv2', 'GlidlerFormAdaptor'], + hook: 'useGridlerStore()', + name: 'Gridler' + }, + MantineBetterMenu: { + description: 'Enhanced context menus with better positioning and visibility control', + examples: { + provider: `import { MantineBetterMenusProvider } from '@warkypublic/oranguru'; +import { MantineProvider } from '@mantine/core'; + +function App() { + return ( + + + + + + ); +}`, + contextMenu: `import { useMantineBetterMenus } from '@warkypublic/oranguru'; + +function MyComponent() { + const { show, hide } = useMantineBetterMenus(); + + const handleContextMenu = (e) => { + e.preventDefault(); + + show('context-menu', { + x: e.clientX, + y: e.clientY, + items: [ + { + label: 'Edit', + onClick: () => console.log('Edit clicked') + }, + { + label: 'Copy', + onClick: () => console.log('Copy clicked') + }, + { + isDivider: true + }, + { + label: 'Delete', + onClick: () => console.log('Delete clicked'), + color: 'red' + } + ] + }); + }; + + return ( +
+ Right-click me for a context menu +
+ ); +}`, + asyncActions: `import { useMantineBetterMenus } from '@warkypublic/oranguru'; + +function AsyncMenuExample() { + const { show } = useMantineBetterMenus(); + + const handleClick = (e) => { + show('async-menu', { + x: e.clientX, + y: e.clientY, + items: [ + { + label: 'Save', + onClickAsync: async () => { + await fetch('/api/save', { method: 'POST' }); + console.log('Saved successfully'); + } + }, + { + label: 'Load Data', + onClickAsync: async () => { + const data = await fetch('/api/data').then(r => r.json()); + console.log('Data loaded:', data); + } + } + ] + }); + }; + + return ; +}`, + customRenderer: `import { useMantineBetterMenus } from '@warkypublic/oranguru'; + +function CustomMenuExample() { + const { show } = useMantineBetterMenus(); + + const handleClick = (e) => { + show('custom-menu', { + x: e.clientX, + y: e.clientY, + items: [ + { + renderer: ({ loading }) => ( +
+ {loading ? 'Processing...' : 'Custom Item'} +
+ ) + } + ] + }); + }; + + return ; +}` + }, + exports: ['MantineBetterMenusProvider', 'useMantineBetterMenus'], + hook: 'useMantineBetterMenus()', + name: 'MantineBetterMenu', + provider: 'MantineBetterMenusProvider' + } +}; + +class OranguruMCPServer { + constructor() { + this.server = new Server( + { + name: 'oranguru-docs', + version: '0.0.31', + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + } + ); + + this.setupHandlers(); + this.server.onerror = (error) => console.error('[MCP Error]', error); + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Oranguru MCP server running on stdio'); + } + + setupHandlers() { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + description: 'Get documentation for a specific Oranguru component', + inputSchema: { + properties: { + component: { + description: 'The component name', + enum: Object.keys(COMPONENTS), + type: 'string', + }, + }, + required: ['component'], + type: 'object', + }, + name: 'get_component_docs', + }, + { + description: 'Generate code example for a specific Oranguru component', + inputSchema: { + properties: { + component: { + description: 'The component name', + enum: Object.keys(COMPONENTS), + type: 'string', + }, + variant: { + description: "Example variant (e.g., 'basic', 'local', 'server')", + type: 'string', + }, + }, + required: ['component'], + type: 'object', + }, + name: 'get_component_example', + }, + { + description: 'List all available Oranguru components', + inputSchema: { + properties: {}, + type: 'object', + }, + name: 'list_components', + }, + ], + })); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { arguments: args, name } = request.params; + + switch (name) { + case 'get_component_docs': + if (!args.component || !COMPONENTS[args.component]) { + throw new Error(`Component ${args.component} not found`); + } + return { + content: [ + { + text: JSON.stringify(COMPONENTS[args.component], null, 2), + type: 'text', + }, + ], + }; + + case 'get_component_example': + if (!args.component || !COMPONENTS[args.component]) { + throw new Error(`Component ${args.component} not found`); + } + const component = COMPONENTS[args.component]; + const variant = args.variant || Object.keys(component.examples)[0]; + const example = component.examples[variant]; + + if (!example) { + throw new Error(`Variant ${variant} not found for ${args.component}`); + } + + return { + content: [ + { + text: example, + type: 'text', + }, + ], + }; + + case 'list_components': + return { + content: [ + { + text: JSON.stringify( + Object.entries(COMPONENTS).map(([key, comp]) => ({ + description: comp.description, + exports: comp.exports, + name: key, + })), + null, + 2 + ), + type: 'text', + }, + ], + }; + + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + + // List resources + this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + description: 'Main documentation for the Oranguru library', + mimeType: 'text/markdown', + name: 'Oranguru Documentation', + uri: 'oranguru://docs/readme', + }, + { + description: 'List of all available components', + mimeType: 'application/json', + name: 'Component List', + uri: 'oranguru://docs/components', + }, + ], + })); + + // Read resources + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + + if (uri === 'oranguru://docs/readme') { + const readmePath = join(__dirname, '..', 'README.md'); + const readme = readFileSync(readmePath, 'utf-8'); + return { + contents: [ + { + mimeType: 'text/markdown', + text: readme, + uri, + }, + ], + }; + } + + if (uri === 'oranguru://docs/components') { + return { + contents: [ + { + mimeType: 'application/json', + text: JSON.stringify( + Object.entries(COMPONENTS).map(([key, comp]) => ({ + description: comp.description, + exports: comp.exports, + name: key, + })), + null, + 2 + ), + uri, + }, + ], + }; + } + + throw new Error(`Resource not found: ${uri}`); + }); + } +} + +const server = new OranguruMCPServer(); +server.run().catch(console.error); diff --git a/package.json b/package.json index da96a3b..88254c0 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,20 @@ "require": "./dist/lib.cjs.js" }, "./oranguru.css": "./dist/oranguru.css", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./mcp": "./mcp-server.json" + }, + "mcp": { + "server": "./mcp/server.js", + "config": "./mcp-server.json" }, "files": [ "dist/**", "assets/**", "public/**", - "global.d.ts" + "global.d.ts", + "mcp/**", + "mcp-server.json" ], "scripts": { "dev": "vite", @@ -33,10 +40,12 @@ "clean": "rm -rf node_modules && rm -rf dist ", "preview": "vite preview", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "mcp": "node mcp/server.js" }, "dependencies": { "@tanstack/react-virtual": "^3.13.18", + "@modelcontextprotocol/sdk": "^1.0.4", "moment": "^2.30.1" }, "devDependencies": { @@ -98,4 +107,4 @@ "use-sync-external-store": ">= 1.4.0", "zustand": ">= 5.0.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6692369..8d67303 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@mantine/notifications': specifier: ^8.3.5 version: 8.3.5(@mantine/core@8.3.1(@mantine/hooks@8.3.1(react@19.2.4))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.1(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@modelcontextprotocol/sdk': + specifier: ^1.0.4 + version: 1.26.0(zod@4.1.12) '@tabler/icons-react': specifier: ^3.35.0 version: 3.35.0(react@19.2.4) @@ -623,6 +626,12 @@ packages: react-dom: ^16.12.0 || 17.x || 18.x react-responsive-carousel: ^3.2.7 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -772,6 +781,16 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -830,56 +849,67 @@ packages: resolution: {integrity: sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.50.2': resolution: {integrity: sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.50.2': resolution: {integrity: sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.50.2': resolution: {integrity: sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.50.2': resolution: {integrity: sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.50.2': resolution: {integrity: sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.50.2': resolution: {integrity: sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.50.2': resolution: {integrity: sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.50.2': resolution: {integrity: sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.2': resolution: {integrity: sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.50.2': resolution: {integrity: sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.50.2': resolution: {integrity: sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==} @@ -1019,24 +1049,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -1364,6 +1398,10 @@ packages: use-sync-external-store: '>= 1.4.0' zustand: '>= 5.0.0' + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1403,6 +1441,9 @@ packages: ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@0.4.14: resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} @@ -1530,6 +1571,10 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1549,6 +1594,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1632,9 +1681,29 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1723,6 +1792,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1772,6 +1845,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.222: resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==} @@ -1785,6 +1861,10 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1841,6 +1921,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1960,10 +2043,32 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -1983,6 +2088,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2007,6 +2115,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2034,6 +2146,14 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.1: resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} @@ -2178,6 +2298,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.11.8: + resolution: {integrity: sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==} + engines: {node: '>=16.9.0'} + html-element-attributes@1.3.1: resolution: {integrity: sha512-UrRKgp5sQmRnDy4TEwAUsu14XBUlzKB8U3hjIYDjcZ3Hbp86Jtftzxfgrv6E/ii/h78tsaZwAnAE8HwnHr0dPA==} @@ -2185,6 +2309,10 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2242,6 +2370,14 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2335,6 +2471,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2408,6 +2547,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2442,6 +2584,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2542,6 +2687,14 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2550,6 +2703,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + min-document@2.19.2: resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} @@ -2611,6 +2772,10 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -2649,6 +2814,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2711,6 +2880,10 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2733,6 +2906,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2759,6 +2935,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -2847,16 +3027,32 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-docgen-typescript@2.4.0: resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} peerDependencies: @@ -3006,6 +3202,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -3049,6 +3249,14 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3061,6 +3269,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3113,6 +3324,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3264,6 +3479,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -3321,6 +3540,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3376,6 +3599,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -3448,6 +3675,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -3638,6 +3869,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -4174,6 +4410,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@hono/node-server@1.19.9(hono@4.11.8)': + dependencies: + hono: 4.11.8 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -4391,6 +4631,28 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@modelcontextprotocol/sdk@1.26.0(zod@4.1.12)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.8) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.8 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.1.12 + zod-to-json-schema: 3.25.1(zod@4.1.12) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5017,6 +5279,11 @@ snapshots: use-sync-external-store: 1.5.0(react@19.2.4) zustand: 5.0.8(@types/react@19.2.10)(immer@10.1.3)(react@19.2.4)(use-sync-external-store@1.5.0(react@19.2.4)) + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5033,6 +5300,10 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5054,6 +5325,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + alien-signals@0.4.14: {} ansi-colors@4.1.3: {} @@ -5181,6 +5459,20 @@ snapshots: dependencies: require-from-string: 2.0.2 + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5206,6 +5498,8 @@ snapshots: dependencies: run-applescript: 7.1.0 + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5280,8 +5574,21 @@ snapshots: confbox@0.2.2: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5368,6 +5675,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + depd@2.0.0: {} + dequal@2.0.3: {} detect-indent@6.1.0: {} @@ -5409,6 +5718,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.222: {} emoji-regex@8.0.0: {} @@ -5417,6 +5728,8 @@ snapshots: empathic@2.0.0: {} + encodeurl@2.0.0: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -5560,6 +5873,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -5776,8 +6091,54 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.2.2: {} + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extendable-error@0.1.7: {} @@ -5796,6 +6157,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -5816,6 +6179,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -5848,6 +6222,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 @@ -6007,6 +6385,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.11.8: {} + html-element-attributes@1.3.1: {} html-encoding-sniffer@6.0.0: @@ -6015,6 +6395,14 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -6067,6 +6455,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -6155,6 +6547,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6227,6 +6621,8 @@ snapshots: jju@1.4.0: {} + jose@6.1.3: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -6272,6 +6668,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -6365,6 +6763,10 @@ snapshots: mdn-data@2.12.2: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -6372,6 +6774,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + min-document@2.19.2: dependencies: dom-walk: 0.1.2 @@ -6423,6 +6831,8 @@ snapshots: natural-orderby@5.0.0: {} + negotiator@1.0.0: {} + node-releases@2.0.21: {} object-assign@4.1.1: {} @@ -6467,6 +6877,10 @@ snapshots: obug@2.1.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6537,6 +6951,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -6552,6 +6968,8 @@ snapshots: lru-cache: 11.2.5 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -6566,6 +6984,8 @@ snapshots: pify@4.0.1: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -6663,12 +7083,30 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + react-docgen-typescript@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -6862,6 +7300,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.50.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -6903,6 +7351,31 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6925,6 +7398,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6978,6 +7453,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -7145,6 +7622,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.17 @@ -7187,6 +7666,12 @@ snapshots: type-fest@4.41.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -7252,6 +7737,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -7311,6 +7798,8 @@ snapshots: uuid@11.1.0: {} + vary@1.1.2: {} + vite-plugin-dts@4.5.4(@types/node@25.2.0)(rollup@4.50.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.6))): dependencies: '@microsoft/api-extractor': 7.56.0(@types/node@25.2.0) @@ -7507,6 +7996,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@4.1.12): + dependencies: + zod: 4.1.12 + zod-validation-error@4.0.2(zod@4.1.12): dependencies: zod: 4.1.12 diff --git a/src/Boxer/hooks/useBoxerOptions.tsx b/src/Boxer/hooks/useBoxerOptions.tsx index 7028ecf..7de38bb 100644 --- a/src/Boxer/hooks/useBoxerOptions.tsx +++ b/src/Boxer/hooks/useBoxerOptions.tsx @@ -1,17 +1,17 @@ -import { Combobox, Checkbox } from '@mantine/core'; +import { Checkbox, Combobox } from '@mantine/core'; import { useMemo } from 'react'; import type { BoxerItem } from '../Boxer.types'; interface UseBoxerOptionsProps { boxerData: Array; - value?: any | Array; multiSelect?: boolean; onOptionSubmit: (index: number) => void; + value?: any | Array; } const useBoxerOptions = (props: UseBoxerOptionsProps) => { - const { boxerData, value, multiSelect, onOptionSubmit } = props; + const { boxerData, multiSelect, onOptionSubmit, value } = props; const options = useMemo(() => { return boxerData.map((item, index) => { @@ -21,15 +21,15 @@ const useBoxerOptions = (props: UseBoxerOptionsProps) => { return ( { onOptionSubmit(index); }} + value={String(index)} > {multiSelect ? ( -
+
{}} tabIndex={-1} /> {item.label}
diff --git a/src/Former/Former.tsx b/src/Former/Former.tsx index 1960cb2..189bf36 100644 --- a/src/Former/Former.tsx +++ b/src/Former/Former.tsx @@ -97,7 +97,7 @@ const FormerInner = forwardRef, Partial> & Props return ( {typeof wrapper === 'function' ? ( - wrapper({props.children}, opened, onClose, onOpen, getState) + wrapper({props.children}, opened ??false, onClose ?? (() => {setState('opened', false)}), onOpen ?? (() => {setState('opened', true)}), getState) ) : ( {props.children || null} )} diff --git a/src/Former/Former.types.ts b/src/Former/Former.types.ts index c5107f2..f895f97 100644 --- a/src/Former/Former.types.ts +++ b/src/Former/Former.types.ts @@ -62,9 +62,9 @@ export interface FormerRef { export type FormerSectionRender = ( children: React.ReactNode, - opened: boolean | undefined, - onClose: ((data?: T) => void) | undefined, - onOpen: ((data?: T) => void) | undefined, + opened: boolean , + onClose: ((data?: T) => void), + onOpen: ((data?: T) => void) , getState: FormerState['getState'] ) => React.ReactNode; diff --git a/src/Former/FormerLayoutBottom.tsx b/src/Former/FormerLayoutBottom.tsx index c07aa7d..35c6fb4 100644 --- a/src/Former/FormerLayoutBottom.tsx +++ b/src/Former/FormerLayoutBottom.tsx @@ -2,19 +2,20 @@ import { useFormerStore } from './Former.store'; import { FormerButtonArea } from './FormerButtonArea'; export const FormerLayoutBottom = () => { - const { buttonArea, getState, opened, renderBottom } = useFormerStore((state) => ({ + const { buttonArea, getState, opened, renderBottom ,setState} = useFormerStore((state) => ({ buttonArea: state.layout?.buttonArea, getState: state.getState, opened: state.opened, renderBottom: state.layout?.renderBottom, + setState: state.setState, })); if (renderBottom) { return renderBottom( , - opened, - getState('onClose'), - getState('onOpen'), + opened ?? false, + getState('onClose') ?? (() => {setState('opened', false)}), + getState('onOpen') ?? (() => {setState('opened', true)}), getState ); } diff --git a/src/Former/FormerLayoutTop.tsx b/src/Former/FormerLayoutTop.tsx index 5bc3369..32cb685 100644 --- a/src/Former/FormerLayoutTop.tsx +++ b/src/Former/FormerLayoutTop.tsx @@ -2,19 +2,20 @@ import { useFormerStore } from './Former.store'; import { FormerButtonArea } from './FormerButtonArea'; export const FormerLayoutTop = () => { - const { buttonArea, getState, opened, renderTop } = useFormerStore((state) => ({ + const { buttonArea, getState, opened, renderTop,setState } = useFormerStore((state) => ({ buttonArea: state.layout?.buttonArea, getState: state.getState, opened: state.opened, renderTop: state.layout?.renderTop, + setState: state.setState, })); if (renderTop) { return renderTop( , - opened, - getState('onClose'), - getState('onOpen'), + opened ?? false, + getState('onClose') ?? (() => {setState('opened', false)}), + getState('onOpen') ?? (() => {setState('opened', true)}), getState ); } diff --git a/src/GlobalStateStore/GlobalStateStore.stories.tsx b/src/GlobalStateStore/GlobalStateStore.stories.tsx new file mode 100644 index 0000000..ecaeb11 --- /dev/null +++ b/src/GlobalStateStore/GlobalStateStore.stories.tsx @@ -0,0 +1,411 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Button, Card, Group, Stack, Switch, Text, TextInput, Title } from '@mantine/core'; +import { useEffect, useState } from 'react'; + +import { + GlobalStateStore, + GlobalStateStoreProvider, + useGlobalStateStore, + useGlobalStateStoreContext, +} from './'; + +// Basic State Display Component +const StateDisplay = () => { + const state = useGlobalStateStore(); + + return ( + + + Current State + +
+ Program: + Name: {state.program.name || '(empty)'} + Slug: {state.program.slug || '(empty)'} +
+ +
+ Session: + API URL: {state.session.apiURL || '(empty)'} + Connected: {state.session.connected ? 'Yes' : 'No'} + Auth Token: {state.session.authToken || '(empty)'} +
+ +
+ Owner: + Name: {state.owner.name || '(empty)'} + ID: {state.owner.id} + Theme: {state.owner.theme?.name || 'none'} + Dark Mode: {state.owner.theme?.darkMode ? 'Yes' : 'No'} +
+ +
+ User: + Username: {state.user.username || '(empty)'} + Email: {state.user.email || '(empty)'} + Theme: {state.user.theme?.name || 'none'} + Dark Mode: {state.user.theme?.darkMode ? 'Yes' : 'No'} +
+ +
+ Layout: + Left Bar: {state.layout.leftBar.open ? 'Open' : 'Closed'} + Right Bar: {state.layout.rightBar.open ? 'Open' : 'Closed'} + Top Bar: {state.layout.topBar.open ? 'Open' : 'Closed'} + Bottom Bar: {state.layout.bottomBar.open ? 'Open' : 'Closed'} +
+
+
+ ); +}; + +// Interactive Controls Component +const InteractiveControls = () => { + const state = useGlobalStateStore(); + const [programName, setProgramName] = useState(''); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + + return ( + + + Controls + +
+ Program + + setProgramName(e.currentTarget.value)} + placeholder="Program name" + value={programName} + /> + + +
+ +
+ User + + + setUsername(e.currentTarget.value)} + placeholder="Username" + value={username} + /> + setEmail(e.currentTarget.value)} + placeholder="Email" + value={email} + /> + + + +
+ +
+ Theme + + + state.setUser({ + theme: { ...state.user.theme, darkMode: e.currentTarget.checked }, + }) + } + /> + + state.setOwner({ + theme: { ...state.owner.theme, darkMode: e.currentTarget.checked }, + }) + } + /> + +
+ +
+ Layout + + state.setLeftBar({ open: e.currentTarget.checked })} + /> + state.setRightBar({ open: e.currentTarget.checked })} + /> + state.setTopBar({ open: e.currentTarget.checked })} + /> + state.setBottomBar({ open: e.currentTarget.checked })} + /> + +
+ +
+ Actions + + + +
+
+
+ ); +}; + +// Provider Context Example +const ProviderExample = () => { + const { refetch } = useGlobalStateStoreContext(); + const state = useGlobalStateStore(); + + return ( + + + Provider Context + API URL: {state.session.apiURL} + Loading: {state.session.loading ? 'Yes' : 'No'} + Connected: {state.session.connected ? 'Yes' : 'No'} + + + + ); +}; + +// Main Story Component +const BasicStory = () => { + useEffect(() => { + // Set initial state for demo + GlobalStateStore.getState().setProgram({ + description: 'A demonstration application', + name: 'Demo App', + slug: 'demo-app', + }); + GlobalStateStore.getState().setOwner({ + id: 1, + name: 'Demo Organization', + theme: { darkMode: false, name: 'light' }, + }); + GlobalStateStore.getState().setUser({ + email: 'demo@example.com', + theme: { darkMode: false, name: 'light' }, + username: 'demo-user', + }); + }, []); + + return ( + + + + + ); +}; + +// Provider Story Component +const ProviderStory = () => { + return ( + + + + + + + + ); +}; + +// Layout Controls Story +const LayoutStory = () => { + const state = useGlobalStateStore(); + + return ( + + + Layout Controls + + + + Left Sidebar + state.setLeftBar({ open: e.currentTarget.checked })} + /> + state.setLeftBar({ pinned: e.currentTarget.checked })} + /> + state.setLeftBar({ collapsed: e.currentTarget.checked })} + /> + + state.setLeftBar({ size: parseInt(e.currentTarget.value) || 0 }) + } + type="number" + value={state.layout.leftBar.size || 0} + /> + + + + Right Sidebar + state.setRightBar({ open: e.currentTarget.checked })} + /> + state.setRightBar({ pinned: e.currentTarget.checked })} + /> + + + + + + + ); +}; + +// Theme Story +const ThemeStory = () => { + const state = useGlobalStateStore(); + + useEffect(() => { + GlobalStateStore.getState().setOwner({ + id: 1, + name: 'Acme Corp', + theme: { darkMode: false, name: 'corporate' }, + }); + }, []); + + return ( + + + Theme Settings + +
+ Owner Theme (Organization Default) + + + state.setOwner({ + theme: { ...state.owner.theme, name: e.currentTarget.value }, + }) + } + value={state.owner.theme?.name || ''} + /> + + state.setOwner({ + theme: { ...state.owner.theme, darkMode: e.currentTarget.checked }, + }) + } + /> + +
+ +
+ User Theme (Personal Override) + + + state.setUser({ + theme: { ...state.user.theme, name: e.currentTarget.value }, + }) + } + value={state.user.theme?.name || ''} + /> + + state.setUser({ + theme: { ...state.user.theme, darkMode: e.currentTarget.checked }, + }) + } + /> + +
+ +
+ Effective Theme + + Name: {state.user.theme?.name || state.owner.theme?.name || 'default'} + + + Dark Mode:{' '} + {(state.user.theme?.darkMode ?? state.owner.theme?.darkMode) ? 'Yes' : 'No'} + +
+
+
+ +
+ ); +}; + +const meta = { + component: BasicStory, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + title: 'State/GlobalStateStore', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: () => , +}; + +export const WithProvider: Story = { + render: () => , +}; + +export const LayoutControls: Story = { + render: () => , +}; + +export const ThemeControls: Story = { + render: () => , +}; diff --git a/src/GlobalStateStore/GlobalStateStore.ts b/src/GlobalStateStore/GlobalStateStore.ts index dfe1fe9..e423344 100644 --- a/src/GlobalStateStore/GlobalStateStore.ts +++ b/src/GlobalStateStore/GlobalStateStore.ts @@ -1,189 +1,250 @@ import type { StoreApi } from 'zustand'; -import { produce } from 'immer'; import { shallow } from 'zustand/shallow'; import { useStoreWithEqualityFn } from 'zustand/traditional'; import { createStore } from 'zustand/vanilla'; -import type { ExtractState, GlobalState, GlobalStateStoreState } from './GlobalStateStore.types'; +import type { + AppState, + BarState, + ExtractState, + GlobalState, + GlobalStateStoreType, + LayoutState, + NavigationState, + OwnerState, + ProgramState, + SessionState, + UserState, +} from './GlobalStateStore.types'; import { loadStorage, saveStorage } from './GlobalStateStore.utils'; -const emptyStore: GlobalState = { - connected: true, //Only invalidate when a connection cannot be made - controls: {}, - environment: 'production', - loading: false, - meta: {}, +const initialState: GlobalState = { + app: { + controls: {}, + environment: 'production', + }, + layout: { + bottomBar: { open: false }, + leftBar: { open: false }, + rightBar: { open: false }, + topBar: { open: false }, + }, + navigation: { + menu: [], + }, + owner: { + id: 0, + name: '', + }, program: { name: '', slug: '', }, - + session: { + apiURL: '', + authToken: '', + connected: true, + loading: false, + }, user: { - access_control: false, - avatar_docid: 0, - id: 0, - login: '', - name: 'Guest', + username: '', }, }; -//We use vanilla store because we must be able to get the API key and token outside a react render loop -//The storage is custom because zustand's vanilla stores persist API crashes. -//Also not using the other store because it's using outdated methods and give that warning +type GetState = () => GlobalStateStoreType; +type SetState = ( + partial: ((state: GlobalState) => Partial) | Partial +) => void; -/** - * A zustand store function for managing program data and session information. - * - * @returns A zustand store state object. - */ -const GlobalStateStore = createStore((set, get) => ({ - ...emptyStore, - fetchData: async (url?: string) => { - const setFetched = async ( - fn: (partial: GlobalState | Partial) => Partial - ) => { - const state = fn(get()); - set((cur) => { - return { ...cur, ...state }; - }); - }; +const createProgramSlice = (set: SetState) => ({ + setProgram: (updates: Partial) => + set((state: GlobalState) => ({ + program: { ...state.program, ...updates }, + })), +}); - try { - set((s) => ({ - ...s, - loading: true, - session: { ...s.session, apiURL: url ?? s.session.apiURL }, - })); - - const result = get().onFetchSession?.(get()); - - await setFetched((s) => ({ - ...s, - ...result, - connected: true, - loading: false, - updatedAt: new Date().toISOString(), - })); - } catch (e) { - await setFetched((s) => ({ - ...s, - connected: false, - error: `Load Exception: ${String(e)}`, - loading: false, - })); - } - }, - - login: async (sessionData?: string) => { - const state = get(); - const newstate = { - ...state, - session: { ...state.session, authtoken: sessionData ?? '' }, - user: { ...state.user }, - }; - - set((cur) => { - return { ...cur, ...newstate }; - }); - await get().fetchData(); - }, - logout: async () => { - const newstate = { ...get(), ...emptyStore }; - - set((state) => { - return { ...state, ...newstate }; - }); - await get().fetchData(); - }, +const createSessionSlice = (set: SetState) => ({ + setApiURL: (url: string) => + set((state: GlobalState) => ({ + session: { ...state.session, apiURL: url }, + })), setAuthToken: (token: string) => - set( - produce((state) => { - state.session.authtoken = token; - }) - ), - setIsSecurity: (issecurity: boolean) => - set( - produce((state) => { - state.session.jsonvalue.issecurity = issecurity; - }) - ), - setState: (key, value) => - set( - produce((state) => { - state[key] = value; - }) - ), - setStateFN: (key, value) => { - set( - produce((state) => { - if (typeof value === 'function') { - state[key] = (value as (value: any) => any)(state[key]); - } else { - console.error('value is not a function', value); - } - }) - ); - }, - updateSession: (setter: UpdateSessionType) => { - const curState = get(); + set((state: GlobalState) => ({ + session: { ...state.session, authToken: token }, + })), - const newSession: null | SessionDetail | void = - typeof setter === 'function' - ? setter(curState?.session) - : typeof setter === 'object' - ? (setter as SessionDetail) - : null; - if (newSession === null) { - return; - } + setSession: (updates: Partial) => + set((state: GlobalState) => ({ + session: { ...state.session, ...updates }, + })), +}); - const updatedState = { - ...curState, - session: { ...curState.session, ...(newSession || {}) }, - }; +const createOwnerSlice = (set: SetState) => ({ + setOwner: (updates: Partial) => + set((state: GlobalState) => ({ + owner: { ...state.owner, ...updates }, + })), +}); - set((state) => { - state = { +const createUserSlice = (set: SetState) => ({ + setUser: (updates: Partial) => + set((state: GlobalState) => ({ + user: { ...state.user, ...updates }, + })), +}); + +const createLayoutSlice = (set: SetState) => ({ + setBottomBar: (updates: Partial) => + set((state: GlobalState) => ({ + layout: { ...state.layout, bottomBar: { ...state.layout.bottomBar, ...updates } }, + })), + + setLayout: (updates: Partial) => + set((state: GlobalState) => ({ + layout: { ...state.layout, ...updates }, + })), + + setLeftBar: (updates: Partial) => + set((state: GlobalState) => ({ + layout: { ...state.layout, leftBar: { ...state.layout.leftBar, ...updates } }, + })), + + setRightBar: (updates: Partial) => + set((state: GlobalState) => ({ + layout: { ...state.layout, rightBar: { ...state.layout.rightBar, ...updates } }, + })), + + setTopBar: (updates: Partial) => + set((state: GlobalState) => ({ + layout: { ...state.layout, topBar: { ...state.layout.topBar, ...updates } }, + })), +}); + +const createNavigationSlice = (set: SetState) => ({ + setCurrentPage: (page: NavigationState['currentPage']) => + set((state: GlobalState) => ({ + navigation: { ...state.navigation, currentPage: page }, + })), + + setMenu: (menu: NavigationState['menu']) => + set((state: GlobalState) => ({ + navigation: { ...state.navigation, menu }, + })), + + setNavigation: (updates: Partial) => + set((state: GlobalState) => ({ + navigation: { ...state.navigation, ...updates }, + })), +}); + +const createAppSlice = (set: SetState) => ({ + setApp: (updates: Partial) => + set((state: GlobalState) => ({ + app: { ...state.app, ...updates }, + })), +}); + +const createComplexActions = (set: SetState, get: GetState) => ({ + fetchData: async (url?: string) => { + try { + set((state: GlobalState) => ({ + session: { + ...state.session, + apiURL: url ?? state.session.apiURL, + loading: true, + }, + })); + + const currentState = get(); + const result = await currentState.onFetchSession?.(currentState); + + set((state: GlobalState) => ({ ...state, - session: { ...state.session, ...updatedState.session }, - }; - return state; - }); + ...result, + app: { + ...state.app, + ...result?.app, + updatedAt: new Date().toISOString(), + }, + session: { + ...state.session, + ...result?.session, + connected: true, + loading: false, + }, + })); + } catch (e) { + set((state: GlobalState) => ({ + session: { + ...state.session, + connected: false, + error: `Load Exception: ${String(e)}`, + loading: false, + }, + })); + } }, + + login: async (authToken?: string) => { + set((state: GlobalState) => ({ + session: { + ...state.session, + authToken: authToken ?? '', + }, + })); + await get().fetchData(); + }, + + logout: async () => { + set((state: GlobalState) => ({ + ...initialState, + session: { + ...initialState.session, + apiURL: state.session.apiURL, + }, + })); + await get().fetchData(); + }, +}); + +const GlobalStateStore = createStore((set, get) => ({ + ...initialState, + ...createProgramSlice(set), + ...createSessionSlice(set), + ...createOwnerSlice(set), + ...createUserSlice(set), + ...createLayoutSlice(set), + ...createNavigationSlice(set), + ...createAppSlice(set), + ...createComplexActions(set, get), })); -//Load storage after the createStore function is executed. -try { - loadStorage() - .then((state) => - GlobalStateStore.setState((s: GlobalStateStoreState) => ({ - ...s, - ...state, - })) - ) - .catch((e) => { - console.error('Error loading storage:', e); - }); - - GlobalStateStore.subscribe((state, previousState) => { - //console.log('subscribe', state, previousState) - saveStorage(state).catch((e) => { - console.error('Error saving storage:', e); - }); - if (state.session.authtoken !== previousState.session.authtoken) { - setAuthTokenAPI(state.session.authtoken); - } +loadStorage() + .then((state) => { + GlobalStateStore.setState((current) => ({ + ...current, + ...state, + session: { + ...current.session, + ...state.session, + connected: true, + loading: false, + }, + })); + }) + .catch((e) => { + console.error('Error loading storage:', e); }); -} catch (e) { - console.error('Error loading storage:', e); -} -/** - * Type-bounded version of useStore with shallow equality build in - */ +GlobalStateStore.subscribe((state) => { + saveStorage(state).catch((e) => { + console.error('Error saving storage:', e); + }); +}); + const createTypeBoundedUseStore = ((store) => (selector) => useStoreWithEqualityFn(store, selector, shallow)) as >( store: S @@ -192,46 +253,26 @@ const createTypeBoundedUseStore = ((store) => (selector) => (selector: (state: ExtractState) => T): T; }; -/** - * Creates a hook to access the state of the `GlobalStateStore` with shallow equality - * checking in the selector function. - * - * @typeParam S - The type of the store - * @param store - The store to be used - * @returns A function that returns the state of the store, or a selected part of it - */ const useGlobalStateStore = createTypeBoundedUseStore(GlobalStateStore); -/** - * Sets the API URL in the program data store state. - * - * @param {string} url - The URL to set as the API URL. - * @return {void} - */ const setApiURL = (url: string) => { - if (typeof GlobalStateStore?.setState !== 'function') { - return; - } - GlobalStateStore.setState((s: GlobalStateStoreState) => ({ - ...s, - session: { - ...s.session, - apiURL: url, - }, - })); + GlobalStateStore.getState().setApiURL(url); }; -/** - * Retrieves the API URL from the session stored in the program data store. - * - * @return {string} The API URL from the session. - */ const getApiURL = (): string => { - if (typeof GlobalStateStore?.setState !== 'function') { - return ''; - } - const s = GlobalStateStore.getState(); - return s.session?.apiURL; + return GlobalStateStore.getState().session.apiURL; }; -export { getApiURL, GlobalStateStore, setApiURL, useGlobalStateStore }; +const getAuthToken = (): string => { + return GlobalStateStore.getState().session.authToken; +}; + +const setAuthToken = (token: string) => { + GlobalStateStore.getState().setAuthToken(token); +}; + +const GetGlobalState = (): GlobalStateStoreType => { + return GlobalStateStore.getState(); +} + +export { getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, setApiURL, setAuthToken, useGlobalStateStore }; diff --git a/src/GlobalStateStore/GlobalStateStore.types.ts b/src/GlobalStateStore/GlobalStateStore.types.ts index eb4e63c..67210d5 100644 --- a/src/GlobalStateStore/GlobalStateStore.types.ts +++ b/src/GlobalStateStore/GlobalStateStore.types.ts @@ -1,4 +1,21 @@ -import { type FunctionComponent } from 'react'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +interface AppState { + controls?: Record; + environment: 'development' | 'production'; + globals?: Record; + updatedAt?: string; +} + +interface BarState { + collapsed?: boolean; + menuItems?: MenuItem[]; + meta?: Record; + open: boolean; + pinned?: boolean; + render?: () => React.ReactNode; + size?: number; + +} type DatabaseDetail = { name?: string; @@ -8,52 +25,108 @@ type DatabaseDetail = { type ExtractState = S extends { getState: () => infer X } ? X : never; interface GlobalState { - [key: string]: any; - apiURL: string; - authtoken: string; - connected?: boolean; - environment?: 'development' | 'production'; - error?: string; - globals?: Record; - lastLoadTime?: string; - loading?: boolean; - menu?: Array; - meta?: ProgramMetaData; - program: ProgramDetail; - - updatedAt?: string; - user: UserDetail; + app: AppState; + layout: LayoutState; + navigation: NavigationState; + owner: OwnerState; + program: ProgramState; + session: SessionState; + user: UserState; } -interface GlobalStateStoreState extends GlobalState { +interface GlobalStateActions { + // Complex actions fetchData: (url?: string) => Promise; - login: (sessionData?: string) => Promise; + + login: (authToken?: string) => Promise; logout: () => Promise; - onFetchSession?: (state: GlobalState) => Promise; + // Callback for custom fetch logic + onFetchSession?: (state: GlobalState) => Promise>; + setApiURL: (url: string) => void; + + // App actions + setApp: (updates: Partial) => void; + setAuthToken: (token: string) => void; - setIsSecurity: (isSecurity: boolean) => void; - setState: (key: K, value: GlobalState[K]) => void; - setStateFN: ( - key: K, - value: (current: GlobalState[K]) => Partial - ) => void; + setBottomBar: (updates: Partial) => void; + setCurrentPage: (page: PageInfo) => void; + + // Layout actions + setLayout: (updates: Partial) => void; + setLeftBar: (updates: Partial) => void; + + setMenu: (menu: MenuItem[]) => void; + + // Navigation actions + setNavigation: (updates: Partial) => void; + + // Owner actions + setOwner: (updates: Partial) => void; + + // Program actions + setProgram: (updates: Partial) => void; + + setRightBar: (updates: Partial) => void; + + // Session actions + setSession: (updates: Partial) => void; + + setTopBar: (updates: Partial) => void; + + // User actions + setUser: (updates: Partial) => void; } -type ProgramDetail = { - backend_version?: string; - biglogolink?: string; - database?: DatabaseDetail; - database_version?: string; - logolink?: string; - name: string; - programSummary?: string; - rid_owner?: number; - slug: string; - version?: string; +interface GlobalStateStoreType extends GlobalState, GlobalStateActions {} + +interface LayoutState { + bottomBar: BarState; + leftBar: BarState; + rightBar: BarState; + topBar: BarState; +} + +type MenuItem = { + [key: string]: any; + children?: MenuItem[]; + icon?: string; + id?: number | string; + label: string; + path?: string; }; -interface ProgramMetaData { - [key: string]: any; +interface NavigationState { + currentPage?: PageInfo; + menu: MenuItem[]; +} + +interface OwnerState { + id: number; + logo?: string; + name: string; + settings?: Record; + theme?: ThemeSettings; +} + +type PageInfo = { + breadcrumbs?: string[]; + meta?: Record; + path?: string; + title?: string; +}; + +interface ProgramState { + backendVersion?: string; + bigLogo?: string; + database?: DatabaseDetail; + databaseVersion?: string; + description?: string; + logo?: string; + meta?: Record; + name: string; + slug: string; + tags?: string[]; + version?: string; } interface ProgramWrapperProps { @@ -66,27 +139,50 @@ interface ProgramWrapperProps { version?: string; } -type UserDetail = { - access_control?: boolean; - avatar_docid?: number; - fullnames?: string; - guid?: string; - id?: number; - isadmin?: boolean; - login?: string; - name?: string; - notice_msg?: string; +interface SessionState { + apiURL: string; + authToken: string; + connected: boolean; + error?: string; + isSecurity?: boolean; + loading: boolean; + meta?: Record; parameters?: Record; - rid_hub?: number; - rid_user?: number; - secuser?: Record; -}; +} + +interface ThemeSettings { + darkMode?: boolean; + name?: string; +} + +interface UserState { + avatarUrl?: string; + email?: string; + fullNames?: string; + guid?: string; + isAdmin?: boolean; + noticeMsg?: string; + parameters?: Record; + rid?: number; + theme?: ThemeSettings; + username: string; +} export type { + AppState, + BarState, ExtractState, GlobalState, - GlobalStateStoreState, - ProgramDetail, + GlobalStateActions, + GlobalStateStoreType, + LayoutState, + MenuItem, + NavigationState, + OwnerState, + PageInfo, + ProgramState, ProgramWrapperProps, - UserDetail, + SessionState, + ThemeSettings, + UserState, }; diff --git a/src/GlobalStateStore/GlobalStateStore.utils.ts b/src/GlobalStateStore/GlobalStateStore.utils.ts index 045df57..3046516 100644 --- a/src/GlobalStateStore/GlobalStateStore.utils.ts +++ b/src/GlobalStateStore/GlobalStateStore.utils.ts @@ -1,109 +1,93 @@ -import { createStore, entries, set, type UseStore } from 'idb-keyval'; +import { get, set } from 'idb-keyval'; + +import type { GlobalState } from './GlobalStateStore.types'; const STORAGE_KEY = 'app-data'; -const initilizeStore = () => { - if (indexedDB) { - try { - return createStore('programdata', 'programdata'); - } catch (e) { - console.error('Failed to initialize indexedDB store: ', STORAGE_KEY, e); - } - } - return null; +const SKIP_PATHS = new Set([ + 'app.controls', + 'session.connected', + 'session.error', + 'session.loading', +]); + +const shouldSkipPath = (path: string): boolean => { + return SKIP_PATHS.has(path); }; -const programDataIndexDBStore: null | UseStore = initilizeStore(); - -const skipKeysCallback = (dataKey: string, dataValue: any) => { - if (typeof dataValue === 'function') { +const filterState = (state: unknown, prefix = ''): unknown => { + if (typeof state === 'function') { return undefined; } - if ( - dataKey === 'loading' || - dataKey === 'error' || - dataKey === 'security' || - dataKey === 'meta' || - dataKey === 'help' - ) { - return undefined; + if (state === null || typeof state !== 'object') { + return state; } - return dataValue; + if (Array.isArray(state)) { + return state.map((item, idx) => filterState(item, `${prefix}[${idx}]`)); + } + + const filtered: Record = {}; + for (const [key, value] of Object.entries(state)) { + const path = prefix ? `${prefix}.${key}` : key; + + if (shouldSkipPath(path) || typeof value === 'function') { + continue; + } + + filtered[key] = filterState(value, path); + } + + return filtered; }; -async function loadStorage(storageKey?: string): Promise { - if (indexedDB) { - try { - const storeValues = await entries(programDataIndexDBStore); - const obj: any = {}; - - storeValues.forEach((arr: string[]) => { - const k = String(arr[0]); - obj[k] = JSON.parse(arr[1]); - }); - - return obj; - } catch (e) { - console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e); - } - } else if (localStorage) { - try { - const storagedata = localStorage.getItem(storageKey ?? STORAGE_KEY); - if (storagedata && storagedata.length > 0) { - const obj = JSON.parse(storagedata, (_dataKey, dataValue) => { - if (typeof dataValue === 'string' && dataValue.startsWith('function')) { - return undefined; - } - return dataValue; - }); - return obj; +async function loadStorage(): Promise> { + try { + if (typeof indexedDB !== 'undefined') { + const data = await get(STORAGE_KEY); + if (data) { + return JSON.parse(data) as Partial; } - return {} as T; - } catch (e) { - console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e); } + } catch (e) { + console.error('Failed to load from IndexedDB, falling back to localStorage:', e); } - return {} as T; + try { + if (typeof localStorage !== 'undefined') { + const data = localStorage.getItem(STORAGE_KEY); + if (data) { + return JSON.parse(data) as Partial; + } + } + } catch (e) { + console.error('Failed to load from localStorage:', e); + } + + return {}; } -async function saveStorage(data: T, storageKey?: string): Promise { - if (indexedDB) { - try { - const keys = Object.keys(data as object).filter( - (key) => - key !== 'loading' && - key !== 'error' && - key !== 'help' && - key !== 'meta' && - key !== 'security' && - typeof data[key as keyof T] !== 'function' - ); - const promises = keys.map((key) => { - return set( - key, - JSON.stringify((data as any)[key], skipKeysCallback) ?? '{}', - programDataIndexDBStore - ); - }); - await Promise.all(promises); - return data; - } catch (e) { - console.error('Failed to save indexedDB storage: ', storageKey ?? STORAGE_KEY, e); - } - } else if (localStorage) { - try { - const dataString = JSON.stringify(data, skipKeysCallback); +async function saveStorage(state: GlobalState): Promise { + const filtered = filterState(state); + const serialized = JSON.stringify(filtered); - localStorage.setItem(storageKey ?? STORAGE_KEY, dataString ?? '{}'); - return data; - } catch (e) { - console.error('Failed to save localStorage storage: ', storageKey ?? STORAGE_KEY, e); + try { + if (typeof indexedDB !== 'undefined') { + await set(STORAGE_KEY, serialized); + return; } + } catch (e) { + console.error('Failed to save to IndexedDB, falling back to localStorage:', e); + } + + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, serialized); + } + } catch (e) { + console.error('Failed to save to localStorage:', e); } - return {} as T; } export { loadStorage, saveStorage }; diff --git a/src/GlobalStateStore/GlobalStateStoreWrapper.tsx b/src/GlobalStateStore/GlobalStateStoreWrapper.tsx index e69de29..c98313e 100644 --- a/src/GlobalStateStore/GlobalStateStoreWrapper.tsx +++ b/src/GlobalStateStore/GlobalStateStoreWrapper.tsx @@ -0,0 +1,107 @@ +import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; + +import type { GlobalStateStoreType } from './GlobalStateStore.types'; + +import { GetGlobalState, GlobalStateStore } from './GlobalStateStore'; + + +interface GlobalStateStoreContextValue { + fetchData: (url?: string) => Promise; + getState: () => GlobalStateStoreType; + refetch: () => Promise; +} + +const GlobalStateStoreContext = createContext(null); + + +interface GlobalStateStoreProviderProps { + apiURL?: string; + autoFetch?: boolean; + children: ReactNode; + fetchOnMount?: boolean; + throttleMs?: number; +} + +export function GlobalStateStoreProvider({ + apiURL, + autoFetch = true, + children, + fetchOnMount = true, + throttleMs = 0, +}: GlobalStateStoreProviderProps) { + const lastFetchTime = useRef(0); + const fetchInProgress = useRef(false); + const mounted = useRef(false); + + + const throttledFetch = useCallback( + async (url?: string) => { + const now = Date.now(); + const timeSinceLastFetch = now - lastFetchTime.current; + + if (fetchInProgress.current) { + return; + } + + if (throttleMs > 0 && timeSinceLastFetch < throttleMs) { + return; + } + + try { + fetchInProgress.current = true; + lastFetchTime.current = now; + await GlobalStateStore.getState().fetchData(url); + } finally { + fetchInProgress.current = false; + } + }, + [throttleMs] + ); + + const refetch = useCallback(async () => { + await throttledFetch(); + }, [throttledFetch]); + + useEffect(() => { + if (apiURL) { + GlobalStateStore.getState().setApiURL(apiURL); + } + }, [apiURL]); + + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + + if (autoFetch && fetchOnMount) { + throttledFetch(apiURL).catch((e) => { + console.error('Failed to fetch on mount:', e); + }); + } + } + }, [apiURL, autoFetch, fetchOnMount, throttledFetch]); + + const context = useMemo(() => { + return { + fetchData: throttledFetch, + getState: GetGlobalState, + refetch, + }; + }, [throttledFetch, refetch]); + + + + return ( + + {children} + + ); +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useGlobalStateStoreContext(): GlobalStateStoreContextValue { + const context = useContext(GlobalStateStoreContext); + if (!context) { + throw new Error('useGlobalStateStoreContext must be used within GlobalStateStoreProvider'); + } + return context; +} diff --git a/src/GlobalStateStore/README.md b/src/GlobalStateStore/README.md new file mode 100644 index 0000000..34f74e7 --- /dev/null +++ b/src/GlobalStateStore/README.md @@ -0,0 +1,105 @@ +# GlobalStateStore + +Zustand-based global state management with automatic persistence. + +## Quick Start + +```tsx +import { + GlobalStateStoreProvider, + useGlobalStateStore, + useGlobalStateStoreContext +} from './GlobalStateStore'; + +// Wrap app with provider +function App() { + return ( + + + + ); +} + +// Use in components +function MyComponent() { + const { program, session, user } = useGlobalStateStore(); + const { refetch } = useGlobalStateStoreContext(); + + return ( +
+ {program.name} + +
+ ); +} + +// Outside React +const apiURL = GlobalStateStore.getState().session.apiURL; +GlobalStateStore.getState().setAuthToken('token'); +``` + +## Provider Props + +- **apiURL** - Initial API URL (optional) +- **autoFetch** - Enable automatic fetching (default: `true`) +- **fetchOnMount** - Fetch data when provider mounts (default: `true`) +- **throttleMs** - Minimum time between fetch calls in milliseconds (default: `0`) + +## Context Hook + +`useGlobalStateStoreContext()` returns: +- **fetchData(url?)** - Throttled fetch function +- **refetch()** - Refetch with current URL + +## State Slices + +- **program** - name, logo, description, tags, version +- **session** - apiURL, authToken, connected, loading, error, parameters, meta +- **owner** - id, name, logo, settings, theme (darkMode, name) +- **user** - username, email, fullNames, isAdmin, avatarUrl, parameters, theme (darkMode, name) +- **layout** - leftBar, rightBar, topBar, bottomBar (each: open, collapsed, pinned, size, menuItems) +- **navigation** - menu, currentPage +- **app** - environment, updatedAt, controls, globals + +## Actions + +**Program:** `setProgram(updates)` +**Session:** `setSession(updates)`, `setAuthToken(token)`, `setApiURL(url)` +**Owner:** `setOwner(updates)` +**User:** `setUser(updates)` +**Layout:** `setLayout(updates)`, `setLeftBar(updates)`, `setRightBar(updates)`, `setTopBar(updates)`, `setBottomBar(updates)` +**Navigation:** `setNavigation(updates)`, `setMenu(items)`, `setCurrentPage(page)` +**App:** `setApp(updates)` +**Complex:** `fetchData(url?)`, `login(token?)`, `logout()` + +## Custom Fetch + +```ts +GlobalStateStore.getState().onFetchSession = async (state) => { + const response = await fetch(`${state.session.apiURL}/session`, { + headers: { Authorization: `Bearer ${state.session.authToken}` } + }); + const data = await response.json(); + + return { + program: { name: data.appName, ... }, + user: { id: data.userId, name: data.userName, ... }, + navigation: { menu: data.menu }, + }; +}; +``` + +## Persistence + +Auto-saves to IndexedDB (localStorage fallback). +Auto-loads on initialization. +Skips transient data: loading states, errors, controls. + +## TypeScript + +Fully typed with exported types: +`GlobalState`, `ProgramState`, `SessionState`, `OwnerState`, `UserState`, `ThemeSettings`, `LayoutState`, `BarState`, `NavigationState`, `AppState` diff --git a/src/GlobalStateStore/index.ts b/src/GlobalStateStore/index.ts index acbf2b7..92a9f3c 100644 --- a/src/GlobalStateStore/index.ts +++ b/src/GlobalStateStore/index.ts @@ -1,9 +1,16 @@ -export { ProgramDataWrapper } from './src/ProgramDataWrapper' export { getApiURL, + getAuthToken, + GetGlobalState, + GlobalStateStore, setApiURL, - programDataStore, - useProgramDataStore, -} from './src/store/ProgramDataStore.store' + setAuthToken, + useGlobalStateStore +} from './GlobalStateStore'; -export type * from './src/types' +export type * from './GlobalStateStore.types'; + +export { + GlobalStateStoreProvider, + useGlobalStateStoreContext, +} from './GlobalStateStoreWrapper'; diff --git a/src/Gridler/stories/Examples.goapi.tsx b/src/Gridler/stories/Examples.goapi.tsx index 65dc1d3..2afd441 100644 --- a/src/Gridler/stories/Examples.goapi.tsx +++ b/src/Gridler/stories/Examples.goapi.tsx @@ -4,6 +4,10 @@ import { useRef, useState } from 'react'; import type { GridlerColumns } from '../components/Column'; +import { FormerDialog } from '../../Former'; +import { NativeSelectCtrl, TextInputCtrl } from '../../FormerControllers'; +import { InlineWrapper } from '../../FormerControllers/Inputs/InlineWrapper'; +import NumberInputCtrl from '../../FormerControllers/Inputs/NumberInputCtrl'; import { GlidlerAPIAdaptorForGoLangv2 } from '../components/adaptors'; import { type GridlerRef } from '../components/GridlerStore'; import { Gridler } from '../Gridler'; @@ -18,12 +22,18 @@ export const GridlerGoAPIExampleEventlog = () => { const [selectRow, setSelectRow] = useState(''); const [values, setValues] = useState>>([]); const [search, setSearch] = useState(''); + const [formProps, setFormProps] = useState<{ onChange?: any; onClose?: any; opened: boolean; request: any; title?: string; values: any; } | null>({ + onChange: (_request: string, data: any) => { ref.current?.refresh({ value: data }); }, + onClose: () => { setFormProps((cv) => ({ ...cv, opened: false, request: null, values: null })) }, + opened: false, + request: null, + values: null, + }); const [sections, setSections] = useState | undefined>(undefined); const columns: GridlerColumns = [ { Cell: (row) => { - const process = `${ - row?.cql2?.length > 0 + const process = `${row?.cql2?.length > 0 ? '🔖' : row?.cql1?.length > 0 ? '📕' @@ -32,7 +42,7 @@ export const GridlerGoAPIExampleEventlog = () => { : row?.status === 2 ? '🔒' : '⚙️' - } ${String(row?.id_process ?? '0')}`; + } ${String(row?.id_process ?? '0')}`; return { data: process, @@ -129,10 +139,29 @@ export const GridlerGoAPIExampleEventlog = () => { changeOnActiveClick={true} descriptionField={'process'} onRequestForm={(request, data) => { - console.log('Form requested', request, data); + setFormProps((cv)=> { + return {...cv, opened: true, request: request as any, values: data as any} + }) }} /> + + + + + + + + + { Goto 2050 - + ); }; diff --git a/src/lib.ts b/src/lib.ts index 61bee52..ff1eed0 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -2,6 +2,7 @@ export * from './Boxer'; export * from './ErrorBoundary'; export * from './Former'; export * from './FormerControllers'; +export * from './GlobalStateStore'; export * from './Gridler'; export {