diff --git a/README.md b/README.md index a36e196fb..b3f8bc311 100644 --- a/README.md +++ b/README.md @@ -12,60 +12,673 @@ # OpenFrame OSS Libraries -**The complete backend foundation for AI-powered MSP platforms.** OpenFrame OSS Libraries provides all shared libraries and service cores needed to build scalable, multi-tenant, event-driven IT management infrastructure that powers the OpenFrame ecosystem. +**The complete backend foundation and shared frontend design system for AI-powered MSP platforms.** OpenFrame OSS Libraries provides all shared libraries, service cores, and the unified UI Kit needed to build scalable, multi-tenant, event-driven IT management infrastructure that powers the OpenFrame ecosystem. [![OpenFrame v0.5.2: Live Demo of AI-Powered IT Management for MSPs](https://img.youtube.com/vi/a45pzxtg27k/maxresdefault.jpg)](https://www.youtube.com/watch?v=a45pzxtg27k) -## πŸš€ What is OpenFrame OSS Lib? +--- + +## Table of Contents + +- [What is OpenFrame OSS Lib?](#-what-is-openframe-oss-lib) +- [Repository Structure](#-repository-structure) +- [Quick Start](#-quick-start) +- [Coding Rules & Conventions](#-coding-rules--conventions) + - [General Rules (All Code)](#general-rules-all-code) + - [Java Backend Rules](#java-backend-rules) + - [Frontend (TypeScript/React) Rules](#frontend-typescriptreact-rules) +- [Component Development Guide](#-component-development-guide) +- [Naming Conventions](#-naming-conventions) +- [Linting & Formatting](#-linting--formatting) +- [Testing Requirements](#-testing-requirements) +- [Git Workflow & Branch Strategy](#-git-workflow--branch-strategy) +- [Pull Request Requirements](#-pull-request-requirements) +- [Contribution Workflow](#-contribution-workflow) +- [Architecture Overview](#-architecture-overview) +- [Technology Stack](#-technology-stack) +- [Community & Support](#-community--support) +- [License](#-license) + +--- + +## What is OpenFrame OSS Lib? -OpenFrame OSS Libraries is the **core backend runtime stack** that enables organizations to build production-ready MSP platforms with modern architecture patterns. It serves as the foundation for [OpenFrame](https://openframe.ai) – Flamingo's unified platform that replaces expensive proprietary software with open-source alternatives enhanced by intelligent automation. +OpenFrame OSS Libraries is the **core runtime stack** that enables organizations to build production-ready MSP platforms with modern architecture patterns. It serves as the foundation for [OpenFrame](https://openframe.ai) -- Flamingo's unified platform that replaces expensive proprietary software with open-source alternatives enhanced by intelligent automation. **Key Capabilities:** - **Multi-tenant architecture** supporting thousands of MSP organizations -- **Event-driven processing** with real-time data enrichment and normalization +- **Event-driven processing** with real-time data enrichment and normalization - **AI-ready infrastructure** for intelligent automation and insights - **Enterprise-grade security** with OAuth2/OIDC compliance - **Scalable data platform** combining MongoDB, Cassandra, Redis, and Apache Pinot +- **Shared UI design system** for consistent interfaces across all platforms + +--- + +## Repository Structure + +This is a **monorepo** containing both Java backend modules and a Node.js frontend design system: + +``` +openframe-oss-lib/ +β”œβ”€β”€ .github/ # CI/CD workflows and PR templates +β”‚ β”œβ”€β”€ workflows/ +β”‚ β”‚ β”œβ”€β”€ test.yml # PR testing (Java + Node) +β”‚ β”‚ β”œβ”€β”€ release.yml # Release pipeline +β”‚ β”‚ β”œβ”€β”€ changes.yml # Change detection +β”‚ β”‚ └── matrix.yml # Test matrix generation +β”‚ └── PULL_REQUEST_TEMPLATE.md +β”‚ +β”œβ”€β”€ openframe-frontend-core/ # Shared UI Kit (TypeScript/React) +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ components/ # React components +β”‚ β”‚ β”‚ β”œβ”€β”€ ui/ # Base UI (Button, Card, Input, Modal...) +β”‚ β”‚ β”‚ β”œβ”€β”€ features/ # Complex business components +β”‚ β”‚ β”‚ β”œβ”€β”€ icons/ # Icon components +β”‚ β”‚ β”‚ β”œβ”€β”€ navigation/ # Header, sidebar, sticky nav +β”‚ β”‚ β”‚ └── toast/ # Toast notification system +β”‚ β”‚ β”œβ”€β”€ hooks/ # Custom React hooks +β”‚ β”‚ β”‚ β”œβ”€β”€ ui/ # UI hooks (useDebounce, useMediaQuery) +β”‚ β”‚ β”‚ β”œβ”€β”€ api/ # Data fetching hooks +β”‚ β”‚ β”‚ └── platform/ # Platform configuration hooks +β”‚ β”‚ β”œβ”€β”€ styles/ # ODS design tokens and CSS +β”‚ β”‚ β”œβ”€β”€ utils/ # Utility functions (cn, platform-config) +β”‚ β”‚ └── types/ # TypeScript type definitions +β”‚ β”œβ”€β”€ biome.json # Biome linter & formatter config +β”‚ β”œβ”€β”€ tailwind.config.ts # Tailwind + ODS typography plugin +β”‚ β”œβ”€β”€ tsconfig.json # TypeScript configuration (strict) +β”‚ └── package.json # NPM package config +β”‚ +β”œβ”€β”€ openframe-api-lib/ # Shared DTOs, filters, mappers (Java) +β”œβ”€β”€ openframe-core/ # Core domain models (Java) +β”œβ”€β”€ openframe-security-core/ # JWT + OAuth BFF (Java) +β”œβ”€β”€ openframe-authorization-service-core/ # OAuth2/OIDC server (Java) +β”œβ”€β”€ openframe-data-mongo/ # MongoDB persistence (Java) +β”œβ”€β”€ openframe-data-redis/ # Redis caching (Java) +β”œβ”€β”€ openframe-data-kafka/ # Kafka messaging (Java) +β”œβ”€β”€ openframe-data-cassandra/ # Cassandra storage (Java) +β”œβ”€β”€ openframe-data-pinot/ # Apache Pinot analytics (Java) +β”œβ”€β”€ openframe-data-nats/ # NATS streams (Java) +β”œβ”€β”€ openframe-gateway-service-core/ # Reactive edge gateway (Java) +β”œβ”€β”€ openframe-stream-service-core/ # Stream processing (Java) +β”œβ”€β”€ openframe-management-service-core/# Connector automation (Java) +β”œβ”€β”€ sdk/ # External integration SDKs +β”‚ β”œβ”€β”€ fleetmdm/ +β”‚ └── tacticalrmm/ +β”œβ”€β”€ docs/ # Documentation +β”œβ”€β”€ CONTRIBUTING.md # Contribution guidelines +β”œβ”€β”€ SECURITY.md # Security policy +β”œβ”€β”€ pom.xml # Maven parent POM +└── renovate.json # Automated dependency updates +``` + +--- + +## Quick Start + +### Java Backend + +**Prerequisites:** Java 21+, Maven 3.6+, Docker & Docker Compose, 8GB RAM minimum + +```bash +git clone https://github.com/flamingo-stack/openframe-oss-lib.git +cd openframe-oss-lib + +docker-compose up -d # Start infrastructure services +mvn clean install -DskipTests # Build all modules +``` + +### Frontend UI Kit + +**Prerequisites:** Node.js 22+, npm + +```bash +cd openframe-frontend-core +npm install + +npm run type-check # TypeScript validation +npm run test # Run tests +npm run storybook # Component explorer on port 6006 +npm run build # Production build +``` + +--- + +## Coding Rules & Conventions + +### General Rules (All Code) + +1. **Zero warnings policy** -- treat warnings as errors in CI +2. **No hardcoded secrets** -- use environment variables or secret managers +3. **Write tests for all new code** -- no PRs without corresponding tests +4. **Keep PRs focused** -- one logical change per PR; avoid mixing refactors with features +5. **Document public APIs** -- JavaDoc for Java, JSDoc for TypeScript +6. **No commented-out code** -- delete it; git history preserves old code +7. **Prefer composition over inheritance** +8. **Fail fast** -- validate at boundaries, trust internal code + +### Java Backend Rules + +#### Code Style +- Follow the **Google Java Style Guide** with OpenFrame-specific additions +- Use **Lombok** annotations: `@RequiredArgsConstructor`, `@Slf4j`, `@Builder` +- Prefer constructor injection via `@RequiredArgsConstructor` over `@Autowired` +- Use `@Validated` on controllers, `@Valid` on request bodies + +#### Security (Critical) +- **All database queries MUST include tenant isolation** -- filter by `tenantId` +- Never expose internal IDs or stack traces in error responses +- Use parameterized queries exclusively (no string concatenation) +- Log at `debug` level for tenant context, never log sensitive data +- See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed security requirements + +#### Patterns +```java +// Standard controller structure +@RestController +@RequestMapping("/api/resources") +@RequiredArgsConstructor +@Validated +@Slf4j +public class ResourceController { + private final ResourceService resourceService; + + @GetMapping("/{id}") + public Resource get( + @PathVariable String id, + @AuthenticationPrincipal AuthPrincipal principal + ) { + return resourceService.findByTenantIdAndId( + principal.getTenantId(), id + ).orElseThrow(() -> new ResourceNotFoundException(id)); + } +} +``` + +### Frontend (TypeScript/React) Rules + +#### TypeScript +- **Strict mode enabled** -- no `any` types without justification +- **Zero TypeScript errors** -- the build must pass `npm run type-check` +- Use `interface` for component props, `type` for unions and utility types +- Always export prop interfaces alongside components +- Use path alias `@/*` for all internal imports (never relative `../../../`) + +#### React +- **All hooks must be called unconditionally** at the top of components (enforced by Biome) +- Use `React.forwardRef` for components that accept refs +- Set `displayName` on all `forwardRef` components +- Use named exports exclusively -- no default exports +- Mark client components with `'use client'` directive +- Prefer `useCallback` and `useMemo` for expensive computations +- Clean up effects: always return cleanup functions from `useEffect` + +```tsx +// Correct hook ordering +export function MyComponent({ filter }: MyComponentProps) { + // 1. ALL hooks first, unconditionally + const router = useRouter(); + const [state, setState] = useState(false); + const data = useQuery({ queryKey: ['data', filter], queryFn: fetchData }); + + // 2. THEN conditional returns + if (!data) return null; + + // 3. Then render + return
{data.title}
; +} +``` + +#### Styling +- **Never use hardcoded hex colors** -- enforced by a custom Biome plugin (`no-hex-colors.grit`) +- Always use ODS design tokens: `bg-ods-bg`, `text-ods-text-primary`, `border-ods-border`, etc. +- Use the `cn()` utility for conditional class merging (clsx + tailwind-merge) +- Use **Class Variance Authority (CVA)** for component variants +- Mobile-first responsive design with Tailwind breakpoints (`md:`, `lg:`) + +```tsx +// Correct: ODS tokens +
+ +// Wrong: hardcoded colors +
+``` + +#### Z-Index Hierarchy +Follow the established stacking order across all components: + +| Layer | Z-Index | Usage | +|-------|---------|-------| +| Tooltips | `z-[2147483647]` | Radix tooltips (max safe integer) | +| Toasts | `z-[9999]` | Toast notifications | +| Modals | `z-[1300]` | Custom Modal component | +| Dropdowns | `z-[9999]` | Dropdown menus | +| Header | `z-[50]` | Fixed navigation header | +| Sidebar overlay | `z-[40]` | Sliding sidebar background | +| Sidebar panel | `z-[45]` | Sliding sidebar content | + +--- + +## Component Development Guide + +### Creating a New UI Component + +1. **Create the file** in the appropriate directory: + - Base component -> `src/components/ui/my-component.tsx` + - Feature component -> `src/components/features/my-component.tsx` + - Icon -> `src/components/icons/my-icon.tsx` + +2. **Follow the standard component pattern:** + +```tsx +'use client'; + +import * as React from 'react'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { cn } from '@/utils/cn'; + +// 1. Define variants with CVA +const myComponentVariants = cva( + // Base classes + 'inline-flex items-center rounded-md transition-colors', + { + variants: { + variant: { + primary: 'bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover', + secondary: 'bg-ods-bg-secondary text-ods-text-primary hover:bg-ods-bg-hover', + }, + size: { + sm: 'h-8 px-3 text-sm', + default: 'h-10 px-4 text-base', + lg: 'h-12 px-6 text-lg', + }, + }, + defaultVariants: { + variant: 'primary', + size: 'default', + }, + }, +); + +// 2. Define props interface (always export) +export interface MyComponentProps + extends React.HTMLAttributes, + VariantProps { + /** Description of what this prop does */ + label: string; + /** Optional icon element */ + icon?: React.ReactNode; +} + +// 3. Use forwardRef for DOM access +const MyComponent = React.forwardRef( + ({ variant, size, className, label, icon, ...props }, ref) => { + return ( +
+ {icon} + {label} +
+ ); + }, +); +MyComponent.displayName = 'MyComponent'; + +// 4. Export component, variants, and types +export { MyComponent, myComponentVariants }; +``` + +3. **Add the export** to the appropriate index file: + +```tsx +// src/components/ui/index.ts +export * from './my-component'; +``` + +4. **Write a Storybook story:** + +```tsx +// src/stories/MyComponent.stories.tsx +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import { MyComponent } from '../components/ui/my-component'; + +const meta = { + title: 'UI/MyComponent', + component: MyComponent, + argTypes: { + variant: { control: 'select', options: ['primary', 'secondary'] }, + size: { control: 'select', options: ['sm', 'default', 'lg'] }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { label: 'Hello', variant: 'primary' }, +}; +``` + +### Component Checklist + +- [ ] Uses ODS design tokens (no hardcoded colors) +- [ ] Accepts `className` prop for customization +- [ ] Uses `cn()` for class merging +- [ ] Has TypeScript props interface exported +- [ ] Uses `forwardRef` if it renders a DOM element +- [ ] Has `displayName` set +- [ ] Is responsive (mobile-first) +- [ ] Has a Storybook story +- [ ] Exported from the appropriate index file +- [ ] Follows accessibility best practices (ARIA labels, keyboard nav) + +--- + +## Naming Conventions + +### File Naming + +| Type | Convention | Example | +|------|------------|---------| +| Components | `kebab-case.tsx` | `date-time-picker.tsx` | +| Hooks | `use-kebab-case.ts` | `use-debounce.ts` | +| Utilities | `kebab-case.ts` | `platform-config.tsx` | +| Types | `kebab-case.ts` | `access-code-cohorts.ts` | +| Stories | `PascalCase.stories.tsx` | `Button.stories.tsx` | +| Tests | `kebab-case.test.ts(x)` | `button.test.tsx` | +| Java classes | `PascalCase.java` | `OrganizationService.java` | + +### Code Naming (Enforced by Biome) + +| Element | Convention | Example | +|---------|------------|---------| +| Variables | `camelCase`, `PascalCase`, or `CONSTANT_CASE` | `userName`, `MaxRetries`, `API_URL` | +| Functions | `camelCase` or `PascalCase` | `getUser()`, `MyComponent()` | +| React components | `PascalCase` | `NavigationSidebar` | +| Hooks | `camelCase` starting with `use` | `useDebounce` | +| Interfaces | `PascalCase` | `ButtonProps` | +| Type aliases | `PascalCase` | `PlatformName` | +| Enums | `PascalCase` | `ViewMode` | +| Enum members | `PascalCase` | `ViewMode.Desktop` | +| Constants | `CONSTANT_CASE` or `camelCase` | `MAX_PAGE_SIZE`, `defaultTimeout` | +| Object keys | `camelCase`, `PascalCase`, `snake_case`, or `CONSTANT_CASE` | Flexible for API compat | +| Classes (Java) | `PascalCase` | `OrganizationService` | +| Methods (Java) | `camelCase` | `findByTenant()` | +| Packages (Java) | `lowercase` | `com.openframe.api` | + +### Branch Naming + +``` +feature/short-description # New features +bugfix/short-description # Bug fixes +docs/short-description # Documentation only +refactor/short-description # Code refactoring +chore/short-description # Build, deps, CI changes +``` + +--- + +## Linting & Formatting + +### Frontend (Biome 2.4.4) + +Biome is the primary linter and formatter for all TypeScript/React code. + +**Key settings:** +- Line width: **120 characters** +- Indent: **2 spaces** +- Line endings: **LF** +- Semicolons: **always** +- Quote style: **single quotes** (JS), **double quotes** (JSX) +- Trailing commas: **all** +- Arrow parens: **as needed** (`x => x` not `(x) => x`) +- Import organization: **automatic** (via `organizeImports`) + +**Enforced linter rules:** + +| Rule | Level | Purpose | +|------|-------|---------| +| `useHookAtTopLevel` | error | React hooks must be called unconditionally | +| `noUnusedVariables` | error | No dead code | +| `noUndeclaredDependencies` | error | All imports must be declared in package.json | +| `useNamingConvention` | error | Enforces naming standards (see table above) | +| `useConst` | error | Use `const` when variable is never reassigned | +| `noParameterAssign` | error | Don't reassign function parameters | +| `useExhaustiveDependencies` | warn | Hook dependency arrays must be complete | +| `noUselessConstructor` | error | Remove empty constructors | +| `useLiteralKeys` | error | Use `obj.key` not `obj['key']` | +| `noRedeclare` | error | No duplicate declarations | +| `no-hex-colors` (plugin) | error | No hardcoded hex colors -- use ODS tokens | + +**Running the linter:** + +```bash +cd openframe-frontend-core + +npm run lint # Check for issues +npm run lint:fix # Auto-fix issues +npm run format # Format all files +npm run format:check # Check formatting without changing files +``` + +### Java Backend + +- Follow **Google Java Style Guide** +- Checkstyle and SpotBugs run in CI +- Maven enforcer plugin validates dependency rules + +--- -## ✨ Features +## Testing Requirements -### πŸ—οΈ **Modular Service-Core Architecture** -Built as a collection of focused service modules, each handling specific responsibilities like authentication, data persistence, event processing, and external integrations. +### Frontend -### πŸ” **Enterprise Security** -- Multi-tenant OAuth2 Authorization Server with per-tenant key pairs -- JWT-based authentication with secure token management -- API key management for external integrations -- Role-based access control (RBAC) with fine-grained permissions -- Reactive edge gateway with advanced security controls +- **Framework:** Vitest with jsdom environment +- **Coverage:** v8 provider +- **Location:** Co-located `*.test.ts(x)` files or `__tests__/` directories -### πŸ“Š **Unified Data Platform** -- **MongoDB** for operational data storage and real-time state -- **Apache Pinot** for real-time analytics and OLAP queries -- **Cassandra** for audit log storage and time-series data -- **Redis** for caching, session management, and distributed locking +```bash +npm run test # Watch mode +npm run test:run # Single run (CI) +npm run test:coverage # With coverage report +``` + +**What to test:** +- Component rendering with different prop combinations +- User interactions (clicks, keyboard events) +- Hook behavior and state transitions +- Utility function edge cases + +### Java Backend + +- **Minimum coverage:** 80% line, 75% branch +- **Unit tests:** `@ExtendWith(MockitoExtension.class)` with Given/When/Then pattern +- **Integration tests:** `@SpringBootTest` for full request/response cycles +- **Security tests:** Mandatory for any multi-tenant changes -- verify tenant isolation + +```bash +mvn clean verify # All tests +mvn test -Dgroups=unit # Unit only +mvn test -Dgroups=integration # Integration only +mvn clean verify -Pcoverage # With coverage +``` + +### CI Pipeline + +Tests run automatically on all PRs to `main`: +- **Java:** Build + test (only when Java files change) +- **Node:** Type check + test + build (only when frontend files change) +- Change detection prevents unnecessary CI runs + +--- + +## Git Workflow & Branch Strategy + +### Commit Message Format + +Follow **Conventional Commits**: + +``` +type(scope): description + +# Types: feat, fix, docs, style, refactor, test, chore +# Scope: module or area (optional) +# Description: imperative mood, present tense, lowercase + +# Examples: +feat(auth): add Google SSO support +fix(security): resolve tenant isolation in organization API +docs(api): update GraphQL schema documentation +refactor(service): extract common pagination logic +test(integration): add organization controller tests +chore(deps): upgrade Spring Boot to 3.3.1 +``` + +### Branch Flow + +``` +main (protected) + β”œβ”€β”€ feature/add-search-filters <- PR required + β”œβ”€β”€ bugfix/fix-tenant-isolation <- PR required + β”œβ”€β”€ docs/update-api-docs <- PR required + └── refactor/extract-service <- PR required +``` + +- `main` is the default branch and is **protected** +- All changes go through pull requests +- Branches are deleted after merge +- Renovate bot handles automated dependency updates + +--- + +## Pull Request Requirements + +### Before Opening a PR + +```bash +# Frontend +cd openframe-frontend-core +npm run lint # No linting errors +npm run type-check # No TypeScript errors +npm run test:run # All tests pass +npm run build # Build succeeds + +# Java +mvn clean verify # Compile + tests pass +``` -### ⚑ **Event-Driven Architecture** -- **Kafka messaging backbone** for reliable event processing -- **NATS streams** for real-time agent communication -- **Debezium CDC** for data synchronization across systems -- **Stream processing engine** for data enrichment and normalization +### PR Title Format + +Follow Conventional Commits in the PR title: + +``` +feat(auth): implement Google SSO integration +fix(security): resolve tenant data leakage in API endpoints +docs(development): add testing guidelines +chore(deps): update dependency X to v2.0 +``` + +### PR Description Template + +Every PR must include: + +```markdown +## Description +Brief description of what this PR accomplishes. + +## Improvements +- Step-by-step list of changes made +- Each change on its own line + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update +- [ ] Refactoring + +## Testing +- [ ] Unit tests added/updated +- [ ] Integration tests added/updated +- [ ] Manual testing completed +- [ ] All existing tests pass + +## Security Considerations (if applicable) +- [ ] Tenant isolation maintained +- [ ] Input validation implemented +- [ ] No sensitive data in logs or errors + +## Checklist +- [ ] Code follows project style guidelines +- [ ] Self-review completed +- [ ] Linter and formatter pass +- [ ] Documentation updated where needed +``` + +### Review Process + +- At least **1 approval** required before merge +- CI must pass (type-check, tests, build) +- Address all review comments before requesting re-review +- Reviewers check: code quality, security, test coverage, naming conventions + +--- -### πŸ€– **Client Agent Orchestration** -- Agent registration and lifecycle management -- Tool integration with popular MSP platforms -- Real-time heartbeat monitoring and health checks -- Automated deployment and version management +## Contribution Workflow -### 🌐 **Comprehensive API Layer** -- **REST APIs** for administrative operations and CRUD workflows -- **GraphQL APIs** for efficient data querying and real-time updates -- **External APIs** for third-party integrations and webhooks -- **WebSocket support** for real-time communication +### First-Time Contributors -## πŸ›οΈ Architecture Overview +1. **Fork** the repository on GitHub +2. **Clone** your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/openframe-oss-lib.git + cd openframe-oss-lib + git remote add upstream https://github.com/flamingo-stack/openframe-oss-lib.git + ``` +3. **Join** the [OpenMSP Slack](https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA) community +4. **Find an issue** labeled `good first issue` or `help wanted` +5. **Create a branch**, make your changes, push, and open a PR -OpenFrame OSS Lib implements a layered, event-driven, multi-tenant architecture designed for horizontal scalability: +### Ongoing Contribution + +```bash +# Stay in sync with upstream +git fetch upstream +git rebase upstream/main + +# Create feature branch +git checkout -b feature/my-change + +# Make changes, commit with conventional commits +git add -p +git commit -m "feat(ui): add tooltip delay prop" + +# Push and create PR +git push origin feature/my-change +``` + +### What Makes a Good Contribution + +- **Focused** -- one logical change per PR +- **Tested** -- includes tests for new/changed behavior +- **Documented** -- public APIs have JSDoc/JavaDoc +- **Backwards compatible** -- or clearly marked as a breaking change +- **Follows conventions** -- passes linter, matches project patterns + +### Path to Maintainer + +Regular contributors can become maintainers by: +1. Demonstrating consistent, high-quality contributions +2. Showing deep understanding of the codebase and architecture +3. Helping other contributors and community members +4. Participating in architectural discussions + +--- + +## Architecture Overview ```mermaid flowchart TD @@ -97,159 +710,69 @@ flowchart TD Management --> NATS["NATS Streams"] ``` -### **Architectural Characteristics** -- βœ… **Multi-tenant by design** – Complete tenant isolation at all layers -- βœ… **OAuth2 + OIDC compliant** – Enterprise identity and access management -- βœ… **Reactive edge gateway** – High-throughput request routing -- βœ… **Event-driven processing** – Kafka + Debezium for reliable data flows -- βœ… **Real-time enrichment** – Stream processing for intelligent data transformation -- βœ… **Analytical storage** – Pinot + Cassandra for reporting and insights -- βœ… **Distributed coordination** – Redis-backed locking and caching - -## πŸ—οΈ Core Modules - -### **API & Contracts** -| Module | Purpose | -|--------|---------| -| **api-lib-contracts-and-services** | Shared DTOs, filters, mappers, and reusable services | -| **api-service-core-controllers-and-graphql** | REST + GraphQL API orchestration layer | - -### **Security & Identity** -| Module | Purpose | -|--------|---------| -| **authorization-service-core** | Multi-tenant OAuth2/OIDC Authorization Server | -| **security-core-and-oauth-bff** | JWT infrastructure + OAuth BFF authentication flow | -| **gateway-service-core** | Reactive edge gateway with JWT + API key validation | - -### **Data & Infrastructure** -| Module | Purpose | -|--------|---------| -| **mongo-persistence-layer** | MongoDB document models + repositories | -| **data-platform-core** | Cassandra + Pinot configuration + analytics repositories | -| **kafka-messaging-layer** | Multi-tenant Kafka infrastructure and CDC models | -| **redis-caching-layer** | Redis caching + tenant-aware key management | - -### **Processing & Integration** -| Module | Purpose | -|--------|---------| -| **stream-processing-service-core** | CDC ingestion, data enrichment, and normalization | -| **management-service-core** | Connector automation, Pinot deployment, scheduled operations | -| **client-agent-service-core** | Agent registration, lifecycle management, NATS listeners | -| **external-api-service-core** | Stable REST API layer for external system integration | - -## πŸš€ Quick Start - -Get OpenFrame OSS Libraries running in 5 minutes: - -### Prerequisites -- **Java 21+** (OpenJDK recommended) -- **Maven 3.6+** for dependency management -- **Docker & Docker Compose** for infrastructure services -- **8GB RAM minimum** (16GB recommended) - -### Installation - -```bash -# 1. Clone the repository -git clone https://github.com/flamingo-stack/openframe-oss-lib.git -cd openframe-oss-lib - -# 2. Start infrastructure services with Docker -docker-compose up -d +**Architectural Characteristics:** +- Multi-tenant by design with complete tenant isolation at all layers +- OAuth2 + OIDC compliant enterprise identity management +- Event-driven processing with Kafka + Debezium +- Real-time analytics with Pinot + Cassandra +- Shared UI Kit (`openframe-frontend-core`) for consistent design across all platforms -# 3. Build all modules -mvn clean install -DskipTests - -# 4. Run the main API service -cd openframe-api-service-core -mvn spring-boot:run -``` - -### Verification - -```bash -# Test the health endpoint -curl http://localhost:8080/health - -# Explore GraphQL schema -open http://localhost:8080/graphiql - -# Check OAuth2 configuration -curl http://localhost:8080/.well-known/openid-configuration -``` +--- -**πŸŽ‰ Success!** You now have a running OpenFrame OSS backend with: -- MongoDB (port 27017) - Primary database -- Redis (port 6379) - Caching and sessions -- Kafka (port 9092) - Event streaming -- API Service (port 8080) - Main application API +## Technology Stack -## πŸ› οΈ Technology Stack +### Backend (Java) -| Component | Technology | Purpose | +| Component | Technology | Version | |-----------|------------|---------| -| **Runtime** | Java 21 | Modern language features, performance | -| **Framework** | Spring Boot 3.3.0 | Application framework, dependency injection | -| **Security** | Spring Authorization Server 1.3.1 | OAuth2/OIDC implementation | -| **Database** | MongoDB, Cassandra, Redis, Pinot | Multi-modal data storage | -| **Messaging** | Apache Kafka, NATS | Event streaming, real-time communication | -| **API** | Netflix DGS 9.0.3 | GraphQL implementation | -| **Build** | Maven 3.x | Dependency management, multi-module builds | - -## πŸ“š Documentation - -πŸ“š See the **[Documentation](./docs/README.md)** for comprehensive guides including: - -- **[Getting Started](./docs/README.md#getting-started)** - Installation, setup, and first steps -- **[Development](./docs/README.md#development)** - Local development setup and workflows -- **[Reference](./docs/README.md#reference)** - Technical architecture and API documentation -- **[Contributing](./CONTRIBUTING.md)** - Guidelines for contributing to the project +| Runtime | Java | 21 | +| Framework | Spring Boot | 3.3.0 | +| Security | Spring Authorization Server | 1.3.1 | +| Database | MongoDB, Cassandra, Redis, Pinot | - | +| Messaging | Apache Kafka, NATS | - | +| API | Netflix DGS (GraphQL) | 9.0.3 | +| Build | Maven | 3.x | -### External Resources +### Frontend (TypeScript) -The **OpenFrame CLI** tool for self-hosting deployment is maintained separately: -- **Repository**: [flamingo-stack/openframe-cli](https://github.com/flamingo-stack/openframe-cli) -- **Documentation**: [CLI Documentation](https://github.com/flamingo-stack/openframe-cli/blob/main/docs/README.md) - -## 🎯 Who Should Use This? - -### **MSP Platform Developers** -Build and customize OpenFrame-powered MSP solutions with comprehensive backend services, APIs, and event-driven architecture. - -### **Enterprise IT Teams** -Deploy secure, scalable IT management infrastructure with modern authentication, multi-tenancy, and real-time analytics. - -### **Integration Partners** -Connect existing tools and workflows using standardized REST/GraphQL APIs and event-driven interfaces. +| Component | Technology | Version | +|-----------|------------|---------| +| Language | TypeScript | 5.8 | +| Framework | React | 18/19 | +| Meta-framework | Next.js | 15-16 | +| Styling | Tailwind CSS | 3.4 | +| Linting | Biome | 2.4.4 | +| Components | Radix UI | Latest | +| Variants | Class Variance Authority | Latest | +| Testing | Vitest | 4.x | +| Stories | Storybook | 10.x | +| Build | tsup | Latest | -### **Open Source Contributors** -Contribute to the evolution of open-source MSP tooling and AI-driven automation infrastructure. +--- -## 🌟 Key Benefits +## Community & Support -- **⚑ Fast Development** - Pre-built service cores eliminate months of foundation work -- **πŸ”’ Enterprise Security** - Production-ready OAuth2/OIDC with multi-tenant isolation -- **πŸ“ˆ Horizontally Scalable** - Event-driven architecture scales to handle millions of events -- **πŸ”Œ Integration Ready** - Standard APIs and webhooks for seamless tool integration -- **πŸ€– AI-Optimized** - Data platform designed for machine learning and automation -- **πŸ’° Cost-Effective** - Replace expensive proprietary solutions with open-source alternatives +- **Slack**: [OpenMSP Community](https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA) -- primary communication channel +- **Issues**: [GitHub Issues](https://github.com/flamingo-stack/openframe-oss-lib/issues) -- bug reports and feature requests +- **Website**: [OpenFrame Documentation](https://www.flamingo.run/openframe) +- **Demos**: [OpenFrame YouTube Channel](https://www.youtube.com/@openframe) -## 🀝 Community & Support +--- -- **πŸ’¬ Slack Community**: [OpenMSP Community](https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA) - Primary communication channel -- **πŸ› Issues**: [GitHub Issues](https://github.com/flamingo-stack/openframe-oss-lib/issues) - Bug reports and feature requests -- **πŸ“– Website**: [OpenFrame Documentation](https://www.flamingo.run/openframe) - Official documentation -- **πŸŽ₯ Demos**: [OpenFrame YouTube Channel](https://www.youtube.com/@openframe) - Product demos and tutorials +## Additional Resources -## πŸ“Ί Learn More +- **[CONTRIBUTING.md](./CONTRIBUTING.md)** -- Detailed contribution guidelines with Java-specific patterns +- **[SECURITY.md](./SECURITY.md)** -- Vulnerability reporting and security policy +- **[Frontend UI Kit README](./openframe-frontend-core/README.md)** -- Package-specific usage and API docs +- **[Documentation](./docs/README.md)** -- Architecture guides, API references, and tutorials -[![OpenFrame Preview Webinar](https://img.youtube.com/vi/bINdW0CQbvY/maxresdefault.jpg)](https://www.youtube.com/watch?v=bINdW0CQbvY) +--- -## πŸ“„ License +## License This project is licensed under the [Flamingo AI Unified License v1.0](LICENSE.md). ---
- Built with πŸ’› by the Flamingo team -
\ No newline at end of file + Built with :yellow_heart: by the Flamingo team +
diff --git a/openframe-frontend-core/.eslintrc.json b/openframe-frontend-core/.eslintrc.json index 5a53b1ed7..d7e754fa4 100644 --- a/openframe-frontend-core/.eslintrc.json +++ b/openframe-frontend-core/.eslintrc.json @@ -1,9 +1,5 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript", - "plugin:storybook/recommended" - ], + "extends": ["next/core-web-vitals", "next/typescript", "plugin:storybook/recommended"], "rules": { "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-explicit-any": "warn", @@ -13,10 +9,7 @@ }, "overrides": [ { - "files": [ - "**/*.ts", - "**/*.tsx" - ], + "files": ["**/*.ts", "**/*.tsx"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", @@ -27,4 +20,4 @@ } } ] -} \ No newline at end of file +} diff --git a/openframe-frontend-core/.storybook/main.ts b/openframe-frontend-core/.storybook/main.ts index 546a44a40..0ac95a889 100644 --- a/openframe-frontend-core/.storybook/main.ts +++ b/openframe-frontend-core/.storybook/main.ts @@ -1,16 +1,14 @@ import type { StorybookConfig } from '@storybook/nextjs-vite'; const config: StorybookConfig = { - "stories": [ - "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - "addons": [], - "framework": { - "name": "@storybook/nextjs-vite", - "options": {} + stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [], + framework: { + name: '@storybook/nextjs-vite', + options: {}, + }, + core: { + disableTelemetry: true, }, - "core": { - "disableTelemetry": true - } }; -export default config; \ No newline at end of file +export default config; diff --git a/openframe-frontend-core/.storybook/preview.ts b/openframe-frontend-core/.storybook/preview.ts index 17654aba2..39b7835f1 100644 --- a/openframe-frontend-core/.storybook/preview.ts +++ b/openframe-frontend-core/.storybook/preview.ts @@ -3,16 +3,15 @@ import type { Preview } from '@storybook/nextjs-vite'; import '../src/styles/storybook-fonts.css'; import '../src/styles/index.css'; - const preview: Preview = { parameters: { controls: { matchers: { - color: /(background|color)$/i, - date: /Date$/i, + color: /(background|color)$/i, + date: /Date$/i, }, }, }, }; -export default preview; \ No newline at end of file +export default preview; diff --git a/openframe-frontend-core/COLOR_SYSTEM_MIGRATION_PLAN.md b/openframe-frontend-core/COLOR_SYSTEM_MIGRATION_PLAN.md new file mode 100644 index 000000000..291344426 --- /dev/null +++ b/openframe-frontend-core/COLOR_SYSTEM_MIGRATION_PLAN.md @@ -0,0 +1,264 @@ +# ODS Color System Migration Plan + +## Goal + +Eliminate the unnecessary semantic abstraction layer (Tier 2/3) and align code 1:1 with Figma design tokens. Enable Tailwind opacity modifiers (`bg-ods-bg/50`). + +--- + +## Current State (Problems) + +``` +Figma tokens (hex) + β†’ Tier 1: --ods-system-greys-background: #161616 + β†’ Tier 2: --color-bg: var(--ods-system-greys-background) ← unnecessary layer + β†’ Tier 3: --bg: var(--color-bg) ← unnecessary layer + β†’ Tailwind: bg: 'var(--color-bg)' ← doesn't support /opacity +``` + +- **3 levels of abstraction** instead of 1 +- Designers work with flat tokens, but the code adds semantics that don't exist in Figma +- Hex format doesn't support Tailwind opacity modifier (`bg-ods-bg/50`) +- `data-app-type` duplicates identical values (bg, text, border) for each platform +- 80+ semantic variables, of which only accent/link are actually overridden per platform + +## Target State + +``` +ods_color_tokens.json (hex) + β†’ script hexβ†’hsl + β†’ Tier 1: --ods-system-greys-background: 0deg 0% 9% (HSL channels) + β†’ Tailwind: bg: 'hsl(var(--ods-system-greys-background) / )' +``` + +- **1 level** β€” flat tokens 1:1 with Figma +- HSL channels for opacity support +- The only abstraction β€” `--ods-accent` for platform theming (3 variables) + +--- + +## Steps + +### Step 1: Build script `generate-ods-colors` + +Create a script that reads `ods_color_tokens.json` and generates CSS with HSL channels. + +**Input** (`ods_color_tokens.json`): +```json +{ + "color": { + "system": { + "greys": { + "background": { "value": "#161616", "type": "color" } + } + } + } +} +``` + +**Output** (`ods-colors.generated.css`): +```css +:root { + --ods-system-greys-background: 0deg 0% 9%; + --ods-system-greys-black: 0deg 0% 13%; + --ods-open-yellow-base: 45deg 100% 52%; + --ods-flamingo-pink-base: 323deg 87% 65%; + --ods-flamingo-cyan-base: 174deg 95% 67%; + /* ... all tokens */ + + /* Social β€” stay as hex, opacity not needed */ + --social-slack: #4a154b; + --social-linkedin: #0a66c2; + /* ... */ +} +``` + +**Location**: `scripts/generate-ods-colors.ts` +**npm script**: `"generate:colors": "tsx scripts/generate-ods-colors.ts"` + +--- + +### Step 2: Platform theming β€” accent only + +Replace all `data-app-type` blocks (80+ variables) with a minimal override. + +**New** (`ods-platform-theme.css`): +```css +:root { + --ods-accent: var(--ods-open-yellow-base); + --ods-accent-hover: var(--ods-open-yellow-hover); + --ods-accent-active: var(--ods-open-yellow-action); +} + +[data-app-type="flamingo"], +[data-app-type="flamingo-teaser"] { + --ods-accent: var(--ods-flamingo-pink-base); + --ods-accent-hover: var(--ods-flamingo-pink-hover); + --ods-accent-active: var(--ods-flamingo-pink-action); +} + +[data-app-type="people-hub"] { + --ods-accent: var(--ods-flamingo-cyan-base); + --ods-accent-hover: var(--ods-flamingo-cyan-hover); + --ods-accent-active: var(--ods-flamingo-cyan-action); +} + +/* ... other platforms */ +``` + +**Delete**: all of Tier 2, Tier 3, `.theme-light`, `.theme-high-contrast` (unused). + +--- + +### Step 3: Update `tailwind.config.ts` + +Map Tailwind classes β†’ direct ODS tokens with opacity support. + +```ts +ods: { + // === Backgrounds (semantic shortcuts β†’ direct tokens) === + bg: 'hsl(var(--ods-system-greys-background) / )', + card: 'hsl(var(--ods-system-greys-black) / )', + 'bg-hover': 'hsl(var(--ods-system-greys-black-hover) / )', + 'bg-active': 'hsl(var(--ods-system-greys-black-action) / )', + 'bg-surface': 'hsl(var(--ods-system-greys-soft-grey) / )', + skeleton: 'hsl(var(--ods-system-greys-black) / )', + divider: 'hsl(var(--ods-system-greys-soft-grey) / )', + + // === Borders === + border: { + DEFAULT: 'hsl(var(--ods-system-greys-soft-grey) / )', + hover: 'hsl(var(--ods-system-greys-soft-grey-hover) / )', + active: 'hsl(var(--ods-system-greys-soft-grey-action) / )', + focus: 'hsl(var(--ods-open-yellow-base) / )', + }, + + // === Text === + text: { + primary: 'hsl(var(--ods-system-greys-white) / )', + secondary: 'hsl(var(--ods-system-greys-grey) / )', + tertiary: 'hsl(var(--ods-system-greys-soft-grey-hover) / )', + muted: 'hsl(var(--ods-system-greys-grey-action) / )', + disabled: 'hsl(var(--ods-system-greys-soft-grey) / )', + 'on-accent': 'hsl(0deg 0% 10% / )', + }, + + // === Accent (the only abstraction β€” for platform theming) === + accent: { + DEFAULT: 'hsl(var(--ods-accent) / )', + hover: 'hsl(var(--ods-accent-hover) / )', + active: 'hsl(var(--ods-accent-active) / )', + }, + + // === Status === + success: { + DEFAULT: 'hsl(var(--ods-attention-green-success) / )', + hover: 'hsl(var(--ods-attention-green-success-hover) / )', + secondary: 'hsl(var(--ods-attention-green-success-secondary) / )', + }, + error: { + DEFAULT: 'hsl(var(--ods-attention-red-error) / )', + hover: 'hsl(var(--ods-attention-red-error-hover) / )', + secondary: 'hsl(var(--ods-attention-red-error-secondary) / )', + }, + warning: { + DEFAULT: 'hsl(var(--ods-attention-yellow-warning) / )', + hover: 'hsl(var(--ods-attention-yellow-warning-hover) / )', + secondary: 'hsl(var(--ods-attention-yellow-warning-secondary) / )', + }, + info: { + DEFAULT: 'hsl(var(--ods-flamingo-cyan-base) / )', + hover: 'hsl(var(--ods-flamingo-cyan-hover) / )', + }, + + // === Raw palette (direct access to Figma tokens) === + yellow: { + base: 'hsl(var(--ods-open-yellow-base) / )', + hover: 'hsl(var(--ods-open-yellow-hover) / )', + action: 'hsl(var(--ods-open-yellow-action) / )', + secondary: 'hsl(var(--ods-open-yellow-secondary) / )', + dark: 'hsl(var(--ods-open-yellow-dark) / )', + light: 'hsl(var(--ods-open-yellow-light) / )', + }, + pink: { + base: 'hsl(var(--ods-flamingo-pink-base) / )', + hover: 'hsl(var(--ods-flamingo-pink-hover) / )', + action: 'hsl(var(--ods-flamingo-pink-action) / )', + secondary: 'hsl(var(--ods-flamingo-pink-secondary) / )', + dark: 'hsl(var(--ods-flamingo-pink-dark) / )', + light: 'hsl(var(--ods-flamingo-pink-light) / )', + }, + cyan: { + base: 'hsl(var(--ods-flamingo-cyan-base) / )', + hover: 'hsl(var(--ods-flamingo-cyan-hover) / )', + action: 'hsl(var(--ods-flamingo-cyan-action) / )', + secondary: 'hsl(var(--ods-flamingo-cyan-secondary) / )', + dark: 'hsl(var(--ods-flamingo-cyan-dark) / )', + light: 'hsl(var(--ods-flamingo-cyan-light) / )', + }, + grey: { + bg: 'hsl(var(--ods-system-greys-background) / )', + black: 'hsl(var(--ods-system-greys-black) / )', + soft: 'hsl(var(--ods-system-greys-soft-grey) / )', + grey: 'hsl(var(--ods-system-greys-grey) / )', + white: 'hsl(var(--ods-system-greys-white) / )', + }, +} +``` + +--- + +### Step 4: Component migration + +Tailwind classes **don't change** β€” `bg-ods-bg`, `text-ods-text-primary`, `border-ods-border` remain the same. Only what's behind them changes (CSS variable β†’ direct token in HSL). + +**Needs migration**: +- Inline `style={{ color: 'var(--color-text-secondary)' }}` β†’ `var(--ods-system-greys-grey)` or Tailwind class +- Direct references to `--color-*` in `.tsx` files (~111 occurrences in 29 files) +- Remove `rgba()` hardcodes where `/opacity` can now be used instead + +**No changes needed**: +- All `bg-ods-*`, `text-ods-*`, `border-ods-*` classes β€” they work as before + +--- + +### Step 5: Remove dead code + +- [ ] Remove Tier 2 (`--color-*` variables) from `ods-colors.css` +- [ ] Remove Tier 3 (`--bg`, `--card`, `--accent` shorthands) from `ods-colors.css` +- [ ] Remove `.theme-light` and `.theme-high-contrast` (unused) +- [ ] Remove all `data-app-type` blocks except accent overrides +- [ ] Remove `ods-dynamic-theming.css` if it duplicates the above +- [ ] Update `index.css` imports + +--- + +### Step 6: Validation + +- [ ] `npm run type-check` β€” zero errors +- [ ] Visual check across all platforms (OpenFrame, Flamingo, Admin Hub) +- [ ] Verify that `bg-ods-bg/50` works +- [ ] Grep for `--color-` β€” should be 0 occurrences in `.tsx` files +- [ ] Grep for `rgba(` β€” replace with Tailwind opacity where possible + +--- + +## File Changes Summary + +| File | Action | +|------|--------| +| `scripts/generate-ods-colors.ts` | **Create** β€” hexβ†’HSL script | +| `src/styles/ods-colors.generated.css` | **Create** β€” generated output | +| `src/styles/ods-platform-theme.css` | **Create** β€” accent-only overrides | +| `src/styles/ods-colors.css` | **Delete** β€” replaced by generated + theme | +| `tailwind.config.ts` | **Edit** β€” direct ODS tokens with `` | +| `src/styles/index.css` | **Edit** β€” update imports | +| `~29 .tsx files` | **Edit** β€” replace `--color-*` refs with `--ods-*` | +| `package.json` | **Edit** β€” add `generate:colors` script | + +## Migration Safety + +- Tailwind class names (`bg-ods-bg`, `text-ods-text-primary`) **remain identical** +- Only the underlying CSS variables change +- Zero visual difference after migration (same colors, same rendering) +- Opacity support is purely additive β€” nothing breaks diff --git a/openframe-frontend-core/DUPLICATE_COMPONENTS_AUDIT.md b/openframe-frontend-core/DUPLICATE_COMPONENTS_AUDIT.md new file mode 100644 index 000000000..8a225ed92 --- /dev/null +++ b/openframe-frontend-core/DUPLICATE_COMPONENTS_AUDIT.md @@ -0,0 +1,206 @@ +# Duplicate Components Audit + +> Generated: 2026-03-11 + +## Summary + +| Category | Duplicate Count | Priority | +|----------|----------------|----------| +| UI components (root vs /ui) | 27 | HIGH | +| Icons (root vs /icons) | 39+ | HIGH | +| Icons (/icons vs /icons-v2-generated) | 30+ | MEDIUM | +| Skeleton/Loading overlaps | 15+ | MEDIUM | +| Stub files (obsolete?) | 6 + 6 markers | LOW | +| Table utility overlaps | 2 | LOW | + +--- + +## 1. UI Components β€” Root vs `/ui` Folder (27 duplicates) + +Components exist in **both** `src/components/` and `src/components/ui/` with identical or near-identical code. The `/ui` versions are canonical. + +| File | Root | UI (canonical) | Notes | +|------|------|----------------|-------| +| accordion.tsx | `components/` | `components/ui/` | Identical | +| alert-dialog.tsx | `components/` | `components/ui/` | Identical | +| alert.tsx | `components/` | `components/ui/` | Identical | +| aspect-ratio.tsx | `components/` | `components/ui/` | Identical | +| badge.tsx | `components/` | `components/ui/` | Identical | +| breadcrumb.tsx | `components/` | `components/ui/` | Identical | +| checkbox.tsx | `components/` | `components/ui/` | Identical | +| chevron-button.tsx | `components/` | `components/ui/` | Identical | +| custom-icons.tsx | `components/` | `components/ui/` | Identical | +| dialog.tsx | `components/` | `components/ui/` | Identical | +| dropdown-menu.tsx | `components/` | `components/ui/` | Identical | +| icons-block.tsx | `components/` | `components/ui/` | Identical | +| input.tsx | `components/` | `components/ui/` | **Different** β€” root has custom styling | +| label.tsx | `components/` | `components/ui/` | Identical | +| menubar.tsx | `components/` | `components/ui/` | Identical | +| navigation-menu.tsx | `components/` | `components/ui/` | Identical | +| progress.tsx | `components/` | `components/ui/` | Identical | +| radio-group.tsx | `components/` | `components/ui/` | Identical | +| select.tsx | `components/` | `components/ui/` | Identical | +| separator.tsx | `components/` | `components/ui/` | Identical | +| skeleton.tsx | `components/` | `components/ui/` | **Different** β€” ui has 8+ variants | +| slider.tsx | `components/` | `components/ui/` | Identical | +| square-avatar.tsx | `components/` | `components/ui/` | Identical | +| switch.tsx | `components/` | `components/ui/` | Identical | +| tabs.tsx | `components/` | `components/ui/` | Identical | +| textarea.tsx | `components/` | `components/ui/` | Identical | +| toggle.tsx | `components/` | `components/ui/` | Identical | + +**Action**: Remove root-level versions, update all imports to `components/ui/`. + +--- + +## 2. Icon Components β€” Root vs `/icons` Folder (39+ duplicates) + +Icon files exist in **both** `src/components/` (root) and `src/components/icons/`. The `/icons` versions typically have enhanced prop support (e.g. `color`, `size` props). + +- about-icon.tsx +- check-circle-icon.tsx +- claude-icon.tsx +- coins-icon.tsx +- community-hub-icon.tsx +- community-icon.tsx +- compare-icon.tsx +- custom-external-link-icon.tsx +- custom-fork-icon.tsx +- custom-license-icon.tsx +- custom-star-icon.tsx +- custom-time-icon.tsx +- donut-icon.tsx +- edit-profile-icon.tsx +- elestio-logo.tsx +- empty-vendor-icon.tsx +- flamingo-logo.tsx +- github-icon.tsx +- google-logo.tsx +- hamburger-icon.tsx +- hubspot-icon.tsx +- icon-utils.tsx +- menu-icon.tsx +- minus-circle-icon.tsx +- moon-icon.tsx +- ms-icon.tsx +- open-source-icon.tsx +- openframe-logo.tsx +- openmsp-logo.tsx +- plus-circle-icon.tsx +- reddit-icon.tsx +- send-icon.tsx +- slack-icon.tsx +- sun-icon.tsx +- user-icon.tsx +- vendor-directory-icon.tsx +- vendors-icon.tsx +- x-icon.tsx +- x-logo.tsx + +**Action**: Keep `/icons` versions (enhanced), remove root-level duplicates, update imports. + +--- + +## 3. Icons β€” `/icons` vs `/icons-v2-generated` (30+ overlaps) + +These two icon systems have overlapping icons with **completely different implementations** (different SVGs, different prop APIs). + +| Icon | `/icons/` | `/icons-v2-generated/` | +|------|-----------|----------------------| +| alert-triangle-icon | Custom SVG | Generated, `size` prop | +| buildings-icon | Custom | Generated | +| check-circle-icon | Custom | Generated | +| coins-icon | Custom | Generated | +| eye-icon | Custom | Generated | +| facebook-icon | Custom | Generated (brand-logos/) | +| figma-icon | Custom | Generated | +| file-check-icon | Custom | Generated | +| folder-shield-icon | Custom | Generated | +| github-icon | 15x14px, `fill` | 24px, multi-path, `size` | +| hand-dollar-icon | Custom | Generated | +| hotel-icon | Custom | Generated | +| image-icon | Custom | Generated | +| info-circle-icon | Custom | Generated | +| instagram-icon | Custom | Generated (brand-logos/) | +| linkedin-icon | Custom | Generated (brand-logos/) | +| minus-circle-icon | Custom | Generated | +| moon-icon | Custom | Generated | +| network-icon | Custom | Generated | +| package-icon | Custom | Generated | +| plus-circle-icon | Custom | Generated | +| search-icon | Custom | Generated | +| shape-circle-dash-icon | Custom | Generated | +| shield-check-icon | Custom | Generated | +| shield-icon | Custom | Generated | +| shield-lock-icon | Custom | Generated | +| sliders-icon | Custom | Generated | +| thumbs-down-icon | Custom | Generated | +| thumbs-up-icon | Custom | Generated | +| user-icon | Custom | Generated | + +**Action**: Decide on one icon system. Options: +- **A)** Standardize on `/icons/` (hand-crafted, fewer) +- **B)** Migrate to `/icons-v2-generated/` (auto-generated, 1600+ icons, consistent API) + +--- + +## 4. Skeleton / Loading Component Overlaps + +Multiple skeleton implementations with overlapping purposes: + +| Location | Component | Purpose | +|----------|-----------|---------| +| `components/skeleton.tsx` | Skeleton | Simple animated div (`bg-muted/60`) | +| `components/ui/skeleton.tsx` | Skeleton + 8 variants | Full system (Text, Card, Grid, Button, Heading, List, Nav) | +| `components/dynamic-skeleton.tsx` | DynamicSkeleton | Dynamic grid skeleton | +| `components/loading/` | 12 specialized files | CardSkeleton, DeviceCardSkeleton, UnifiedSkeleton, etc. | +| `components/ui/table/table-skeleton.tsx` | TableSkeleton | Table-specific | +| `components/ui/table/query-report-table/...skeleton.tsx` | QueryReportTableSkeleton | Report-specific | + +**Action**: Consolidate β€” use `ui/skeleton.tsx` as the base, evaluate if specialized loading components can use its variants. + +--- + +## 5. Stub Files (Migration Leftovers) + +Stub files that may no longer be needed if real implementations exist: + +| Stub | Marker | Notes | +|------|--------|-------| +| `components/auth-stub.tsx` | `.auth-stub.md` | Real auth in parent project | +| `components/icons-stub.tsx` | `.icons-stub.md` | Icons exist in `/icons/` | +| `components/join-waitlist-button-stub.tsx` | `.join-waitlist-button-stub.md` | Real button exists | +| `components/user-summary-stub.tsx` | `.user-summary-stub.md` | Real in features | +| `components/ui/pagination-stub.tsx` | `.pagination-stub.md` | `pagination.tsx` exists | +| `components/ui/responsive-icons-block-stub.tsx` | `.responsive-icons-block-stub.md` | `icons-block.tsx` exists | + +**Action**: Verify real implementations are complete, then remove stubs and marker `.md` files. + +--- + +## 6. Table Utility / Type Overlaps + +| File | Location | +|------|----------| +| utils.ts | `components/ui/table/utils.ts` | +| utils.ts | `components/ui/table/query-report-table/utils.ts` | +| types.ts | `components/ui/table/types.ts` | +| types.ts | `components/ui/table/query-report-table/types.ts` | + +**Action**: Audit for shared logic; consolidate if overlapping. + +--- + +## Recommended Cleanup Order + +1. **Phase 1 β€” Root UI duplicates** (27 files): Safest win, clearly identical files +2. **Phase 2 β€” Root icon duplicates** (39 files): `/icons/` versions are strictly better +3. **Phase 3 β€” Stub removal** (6 files + 6 markers): Verify and remove +4. **Phase 4 β€” Skeleton consolidation**: Requires careful import tracing +5. **Phase 5 β€” Icon system unification** (`/icons` vs `/icons-v2-generated`): Architectural decision needed + +## Scale + +- **~100 files** can be safely removed (root UI + root icons + stubs) +- **~30 icons** need architectural decision (which icon system to keep) +- **~15 skeleton files** need consolidation review diff --git a/openframe-frontend-core/biome.json b/openframe-frontend-core/biome.json new file mode 100644 index 000000000..7fc14c7f7 --- /dev/null +++ b/openframe-frontend-core/biome.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json", + "plugins": ["./plugins/no-hex-colors.grit"], + "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!**/node_modules/", + "!**/dist", + "!**/.tsbuildinfo", + "!**/.next", + "!**/.yalc", + "!**/next-env.d.ts", + "!**/*.css" + ] + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "json": { + "formatter": { "enabled": true, "indentWidth": 2, "indentStyle": "space" } + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120, + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noUselessConstructor": "error", + "useLiteralKeys": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "noUndeclaredDependencies": "error", + "useHookAtTopLevel": "error", + "useExhaustiveDependencies": "warn" + }, + "style": { + "noParameterAssign": "error", + "useArrayLiterals": "error", + "useConst": "error", + "useDefaultParameterLast": "error", + "useNamingConvention": { + "level": "error", + "options": { + "strictCase": false, + "conventions": [ + { + "selector": { "kind": "variable" }, + "formats": ["camelCase", "CONSTANT_CASE", "PascalCase"] + }, + { + "selector": { "kind": "function" }, + "formats": ["camelCase", "PascalCase"] + }, + { "selector": { "kind": "class" }, "formats": ["PascalCase"] }, + { + "selector": { "kind": "interface" }, + "formats": ["PascalCase"] + }, + { + "selector": { "kind": "typeAlias" }, + "formats": ["PascalCase"] + }, + { "selector": { "kind": "enum" }, "formats": ["PascalCase"] }, + { + "selector": { "kind": "enumMember" }, + "formats": ["PascalCase"] + }, + { + "selector": { "kind": "objectLiteralMember" }, + "formats": ["camelCase", "PascalCase", "snake_case", "CONSTANT_CASE"] + }, + { + "selector": { "kind": "typeMember" }, + "formats": ["camelCase", "PascalCase", "snake_case", "CONSTANT_CASE"] + }, + { + "selector": { "kind": "importAlias" }, + "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] + }, + { + "selector": { "kind": "exportAlias" }, + "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] + } + ] + } + } + }, + "suspicious": { + "noDuplicateClassMembers": "error", + "noRedeclare": "error" + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "asNeeded", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + } + }, + "css": { + "parser": { "cssModules": true }, + "linter": { "enabled": true }, + "formatter": { "enabled": true } + }, + "overrides": [ + { + "includes": ["**/*.test.ts", "**/*.test.tsx"], + "linter": { + "rules": { "correctness": { "noUndeclaredDependencies": "off" } } + } + }, + { + "includes": ["**/*.css"], + "linter": { "enabled": false }, + "formatter": { "enabled": false } + } + ] +} diff --git a/openframe-frontend-core/package.json b/openframe-frontend-core/package.json index 3b8783ee3..b0cb02dd5 100644 --- a/openframe-frontend-core/package.json +++ b/openframe-frontend-core/package.json @@ -141,7 +141,11 @@ "yalc:publish": "yalc publish", "generate:icons": "node scripts/generate-icons.mjs", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "lint:biome": "biome check .", + "lint:biome:fix": "biome check --fix .", + "format": "biome format .", + "format:fix": "biome format --fix ." }, "peerDependencies": { "@tanstack/react-query": "^5.0.0", @@ -211,6 +215,7 @@ "vaul": "^1.1.2" }, "devDependencies": { + "@biomejs/biome": "^2.4.4", "@storybook/nextjs-vite": "^10.1.11", "@svgr/cli": "^8.1.0", "@testing-library/jest-dom": "^6.9.1", diff --git a/openframe-frontend-core/plugins/no-hex-colors.grit b/openframe-frontend-core/plugins/no-hex-colors.grit new file mode 100644 index 000000000..fdd6578fc --- /dev/null +++ b/openframe-frontend-core/plugins/no-hex-colors.grit @@ -0,0 +1,16 @@ +language js + +or { + `$val` where { + $val <: r"[\"']#[0-9a-fA-F]{3,8}[\"']", + not $val <: within `$body` , + not $val <: within `` , + register_diagnostic(span=$val, message="No hardcoded hex colors. Use ODS design tokens instead (e.g., text-ods-accent, bg-ods-bg-secondary).", severity="warn") + }, + `$val` where { + $val <: r".*\[#[0-9a-fA-F]{3,8}\].*", + not $val <: within `$body` , + not $val <: within `` , + register_diagnostic(span=$val, message="No hardcoded hex colors in Tailwind classes. Use CSS variables instead (e.g., bg-[var(--ods-...)]).") + } +} diff --git a/openframe-frontend-core/scripts/generate-icons.mjs b/openframe-frontend-core/scripts/generate-icons.mjs index 9c1843f36..99f1dd717 100644 --- a/openframe-frontend-core/scripts/generate-icons.mjs +++ b/openframe-frontend-core/scripts/generate-icons.mjs @@ -5,9 +5,9 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, import { basename, dirname, join } from 'path'; import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const rootDir = join(__dirname, '..'); +const Filename = fileURLToPath(import.meta.url); +const Dirname = dirname(Filename); +const rootDir = join(Dirname, '..'); const ICONS_V2_DIR = join(rootDir, 'src/components/icons-v2'); const OUTPUT_DIR = join(rootDir, 'src/components/icons-v2-generated'); @@ -16,7 +16,7 @@ const OUTPUT_DIR = join(rootDir, 'src/components/icons-v2-generated'); function toPascalCase(str) { return str .split(/[-\s]+/) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); } @@ -27,7 +27,7 @@ function toKebabCase(str) { // Get all category directories function getCategories() { - return readdirSync(ICONS_V2_DIR).filter((item) => { + return readdirSync(ICONS_V2_DIR).filter(item => { const itemPath = join(ICONS_V2_DIR, item); return statSync(itemPath).isDirectory() && !item.startsWith('.'); }); @@ -44,8 +44,8 @@ function getOriginalSvgNames(categoryPath) { if (/^\d/.test(name)) { throw new Error( `Invalid icon name: "${name}" starts with a number. ` + - `Icon names cannot start with numbers as they would generate invalid JavaScript/TypeScript identifiers. ` + - `Please rename the file to start with a letter (e.g., "point-100" instead of "100-point").` + `Icon names cannot start with numbers as they would generate invalid JavaScript/TypeScript identifiers. ` + + `Please rename the file to start with a letter (e.g., "point-100" instead of "100-point").`, ); } } @@ -111,9 +111,10 @@ function processGeneratedCategory(categoryPath, originalNames) { exports.sort((a, b) => a.componentName.localeCompare(b.componentName)); // Generate index.ts - const indexContent = exports - .map(({ componentName, kebabName }) => `export { ${componentName} } from './${kebabName}-icon';`) - .join('\n') + '\n'; + const indexContent = + exports + .map(({ componentName, kebabName }) => `export { ${componentName} } from './${kebabName}-icon';`) + .join('\n') + '\n'; writeFileSync(join(categoryPath, 'index.ts'), indexContent); @@ -149,10 +150,10 @@ for (const category of categories) { // Run SVGR for entire category at once try { - execSync( - `npx @svgr/cli --config-file svgr.config.cjs --out-dir "${outputPath}" -- "${inputPath}"`, - { cwd: rootDir, stdio: 'pipe' } - ); + execSync(`npx @svgr/cli --config-file svgr.config.cjs --out-dir "${outputPath}" -- "${inputPath}"`, { + cwd: rootDir, + stdio: 'pipe', + }); // Post-process generated files const count = processGeneratedCategory(outputPath, originalNames); @@ -165,8 +166,8 @@ for (const category of categories) { // Generate root index.ts const categoryExports = categories - .filter((cat) => existsSync(join(OUTPUT_DIR, cat, 'index.ts'))) - .map((cat) => `export * from './${cat}';`) + .filter(cat => existsSync(join(OUTPUT_DIR, cat, 'index.ts'))) + .map(cat => `export * from './${cat}';`) .join('\n'); writeFileSync(join(OUTPUT_DIR, 'index.ts'), categoryExports + '\n'); diff --git a/openframe-frontend-core/src/assets/index.ts b/openframe-frontend-core/src/assets/index.ts index f0b6328c7..eaa60bb29 100644 --- a/openframe-frontend-core/src/assets/index.ts +++ b/openframe-frontend-core/src/assets/index.ts @@ -2,4 +2,4 @@ // Font files and other static assets // Platform utilities will be available when needed -export const assets = {} as const; \ No newline at end of file +export const assets = {} as const; diff --git a/openframe-frontend-core/src/components/about-icon.tsx b/openframe-frontend-core/src/components/about-icon.tsx index c20237cf8..6505ef561 100644 --- a/openframe-frontend-core/src/components/about-icon.tsx +++ b/openframe-frontend-core/src/components/about-icon.tsx @@ -6,11 +6,7 @@ interface AboutIconProps { className?: string; } -export function AboutIcon({ - width = 24, - height = 24, - className = "" -}: AboutIconProps) { +export function AboutIcon({ width = 24, height = 24, className = '' }: AboutIconProps) { return ( ); -} \ No newline at end of file +} diff --git a/openframe-frontend-core/src/components/accordion.tsx b/openframe-frontend-core/src/components/accordion.tsx index 0bf4dd33b..1045ac307 100644 --- a/openframe-frontend-core/src/components/accordion.tsx +++ b/openframe-frontend-core/src/components/accordion.tsx @@ -1,20 +1,20 @@ -"use client" +'use client'; -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDown } from 'lucide-react'; +import * as React from 'react'; -import { cn } from "../utils/cn" +import { cn } from '../utils/cn'; -const Accordion = AccordionPrimitive.Root +const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" + +)); +AccordionItem.displayName = 'AccordionItem'; const AccordionTrigger = React.forwardRef< React.ElementRef, @@ -24,7 +24,7 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-180", + 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180', className, )} {...props} @@ -33,8 +33,8 @@ const AccordionTrigger = React.forwardRef< -)) -AccordionTrigger.displayName = "AccordionTrigger" +)); +AccordionTrigger.displayName = 'AccordionTrigger'; const AccordionContent = React.forwardRef< React.ElementRef, @@ -43,14 +43,14 @@ const AccordionContent = React.forwardRef<
{children}
-)) -AccordionContent.displayName = "AccordionContent" +)); +AccordionContent.displayName = 'AccordionContent'; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/openframe-frontend-core/src/components/alert-dialog.tsx b/openframe-frontend-core/src/components/alert-dialog.tsx index 42d8eafc2..4da63b4fe 100644 --- a/openframe-frontend-core/src/components/alert-dialog.tsx +++ b/openframe-frontend-core/src/components/alert-dialog.tsx @@ -1,16 +1,16 @@ -"use client" +'use client'; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import * as React from 'react'; -import { cn } from "../utils/cn" -import { buttonVariants } from "./ui/button" +import { cn } from '../utils/cn'; +import { buttonVariants } from './ui/button'; -const AlertDialog = AlertDialogPrimitive.Root +const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogPortal = AlertDialogPrimitive.Portal +const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< React.ElementRef, @@ -18,14 +18,14 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, @@ -36,79 +36,48 @@ const AlertDialogContent = React.forwardRef< -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = 'AlertDialogHeader'; -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = 'AlertDialogFooter'; const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -116,15 +85,11 @@ const AlertDialogCancel = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -138,4 +103,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/openframe-frontend-core/src/components/alert.tsx b/openframe-frontend-core/src/components/alert.tsx index 23b4ae5bb..18fb9f780 100644 --- a/openframe-frontend-core/src/components/alert.tsx +++ b/openframe-frontend-core/src/components/alert.tsx @@ -1,59 +1,43 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; -import { cn } from "../utils/cn" +import { cn } from '../utils/cn'; const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', { variants: { variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + default: 'bg-background text-foreground', + destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', }, }, defaultVariants: { - variant: "default", + variant: 'default', }, - } -) + }, +); const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" +
+)); +Alert.displayName = 'Alert'; -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertTitle.displayName = 'AlertTitle'; -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertDescription.displayName = 'AlertDescription'; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/openframe-frontend-core/src/components/announcement-bar.tsx b/openframe-frontend-core/src/components/announcement-bar.tsx index 8aff828bf..ecb26d12b 100644 --- a/openframe-frontend-core/src/components/announcement-bar.tsx +++ b/openframe-frontend-core/src/components/announcement-bar.tsx @@ -1,27 +1,16 @@ -"use client"; +'use client'; -import { useState, useEffect } from 'react'; import { X } from 'lucide-react'; -import { Button } from './ui/button'; -import { renderSvgIcon } from './icon-utils'; -import { - setStoredAnnouncement, - getStoredAnnouncement, - clearStoredAnnouncement, -} from '../utils/announcement-storage'; +import { useCallback, useEffect, useState } from 'react'; import { Announcement } from '../types/announcement'; +import { clearStoredAnnouncement, getStoredAnnouncement, setStoredAnnouncement } from '../utils/announcement-storage'; import { getAppType } from '../utils/app-config'; +import { renderSvgIcon } from './icon-utils'; +import { Button } from './ui/button'; // Helper that defers to renderSvgIcon so we don't need local icon imports -const getSvgIcon = ( - name: string, - size: 'main' | 'cta' = 'main', - extra: Record = {} -) => { - const cls = - size === 'cta' - ? 'relative shrink-0 w-3 h-3 md:w-4 md:h-4' - : 'relative shrink-0 w-6 h-6 md:w-8 md:h-8'; +const getSvgIcon = (name: string, size: 'main' | 'cta' = 'main', extra: Record = {}) => { + const cls = size === 'cta' ? 'relative shrink-0 w-3 h-3 md:w-4 md:h-4' : 'relative shrink-0 w-6 h-6 md:w-8 md:h-8'; return renderSvgIcon(name, { className: cls, ...extra }); }; @@ -33,17 +22,17 @@ export function AnnouncementBar() { const platform = getAppType(); // Helper to determine dismissal key for localStorage - const getDismissKey = (id: string) => `${platform}-announcement-${id}-dismissed`; - + const getDismissKey = useCallback((id: string) => `${platform}-announcement-${id}-dismissed`, [platform]); + // Helper to get platform-specific cache key - const getCacheKey = () => `${platform}-announcement-cache`; + const getCacheKey = useCallback(() => `${platform}-announcement-cache`, [platform]); // Fetch active announcement from API and update state + LS - const fetchActiveAnnouncement = async () => { + const fetchActiveAnnouncement = useCallback(async () => { try { // Server-side platform injection - no URL parameter needed const response = await fetch(`/api/announcements/active`); - + if (response.ok) { const data = await response.json(); if (data.announcement) { @@ -59,7 +48,7 @@ export function AnnouncementBar() { // No announcement available - clean up localStorage and hide bar setAnnouncement(null); setIsVisible(false); - + // Use utility function to properly clear platform-specific announcement data clearStoredAnnouncement(getCacheKey()); } @@ -68,7 +57,7 @@ export function AnnouncementBar() { console.error(`❌ [${platform.toUpperCase()}] Error fetching announcement: ${response.status}`); setAnnouncement(null); setIsVisible(false); - + // Clear stale data on network errors too clearStoredAnnouncement(getCacheKey()); } @@ -76,11 +65,11 @@ export function AnnouncementBar() { console.error('Error fetching active announcement:', error); setAnnouncement(null); setIsVisible(false); - + // Clear stale data on exceptions too clearStoredAnnouncement(getCacheKey()); } - }; + }, [getCacheKey, getDismissKey, platform]); // Initial load: use cached announcement synchronously for instant paint useEffect(() => { @@ -97,7 +86,7 @@ export function AnnouncementBar() { // Schedule refresh every 5 minutes const interval = setInterval(fetchActiveAnnouncement, 300_000); return () => clearInterval(interval); - }, []); + }, [fetchActiveAnnouncement, getCacheKey, getDismissKey]); // helpers const handleDismiss = () => { @@ -127,11 +116,7 @@ export function AnnouncementBar() { ); } - return getSvgIcon( - announcement.icon_svg_name || 'openframe-logo', - 'main', - announcement.icon_svg_props ?? {} - ); + return getSvgIcon(announcement.icon_svg_name || 'openframe-logo', 'main', announcement.icon_svg_props ?? {}); }; // If no announcement or dismissed => render nothing @@ -145,11 +130,11 @@ export function AnnouncementBar() { >
{/* Mobile: Clickable content area, Desktop: Regular content */} -
{ + onClick={e => { // Only handle click on mobile (< 768px) and if CTA is enabled if (window.innerWidth < 768 && announcement.cta_enabled && announcement.cta_url) { e.preventDefault(); @@ -160,10 +145,10 @@ export function AnnouncementBar() { {renderIcon()}
-

+

{announcement.title}

-

+

{announcement.description}

@@ -177,11 +162,7 @@ export function AnnouncementBar() { size="sm" leftIcon={ announcement.cta_show_icon && announcement.cta_icon - ? getSvgIcon( - announcement.cta_icon, - 'cta', - announcement.cta_icon_props ?? {} - ) + ? getSvgIcon(announcement.cta_icon, 'cta', announcement.cta_icon_props ?? {}) : undefined } className="transition-opacity hover:opacity-90 text-xs md:text-sm whitespace-nowrap" @@ -199,15 +180,15 @@ export function AnnouncementBar() { {/* Dismiss button - always visible */}
diff --git a/openframe-frontend-core/src/components/aspect-ratio.tsx b/openframe-frontend-core/src/components/aspect-ratio.tsx index d6a5226f5..aaabffbc4 100644 --- a/openframe-frontend-core/src/components/aspect-ratio.tsx +++ b/openframe-frontend-core/src/components/aspect-ratio.tsx @@ -1,7 +1,7 @@ -"use client" +'use client'; -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; -const AspectRatio = AspectRatioPrimitive.Root +const AspectRatio = AspectRatioPrimitive.Root; -export { AspectRatio } +export { AspectRatio }; diff --git a/openframe-frontend-core/src/components/auth-stub.tsx b/openframe-frontend-core/src/components/auth-stub.tsx index 066e40c0e..c236fb52b 100644 --- a/openframe-frontend-core/src/components/auth-stub.tsx +++ b/openframe-frontend-core/src/components/auth-stub.tsx @@ -1,4 +1,4 @@ -"use client" +'use client'; // Stub auth provider and hooks import { createContext, useContext } from 'react'; @@ -28,7 +28,7 @@ export function useAuth() { if (realAuth && realAuth.user) { return realAuth; } - } catch (error) { + } catch (_error) { // Fallback if real auth fails } } @@ -36,7 +36,7 @@ export function useAuth() { // Fallback mock user for UI kit context return { user: { id: 'mock-user-id', name: 'Mock User' }, - isLoading: false + isLoading: false, }; } @@ -46,4 +46,4 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { {children as any} ); -} \ No newline at end of file +} diff --git a/openframe-frontend-core/src/components/avatar.tsx b/openframe-frontend-core/src/components/avatar.tsx index 5813c20ff..70e3f85a6 100644 --- a/openframe-frontend-core/src/components/avatar.tsx +++ b/openframe-frontend-core/src/components/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +'use client'; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import * as React from 'react'; -import { cn } from "../utils/cn" +import { cn } from '../utils/cn'; const Avatar = React.forwardRef< React.ElementRef, @@ -11,19 +11,19 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -31,10 +31,10 @@ const AvatarFallback = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/openframe-frontend-core/src/components/badge.tsx b/openframe-frontend-core/src/components/badge.tsx index c5f7c8916..f8cf5d492 100644 --- a/openframe-frontend-core/src/components/badge.tsx +++ b/openframe-frontend-core/src/components/badge.tsx @@ -1,30 +1,30 @@ -import type * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from 'class-variance-authority'; +import type * as React from 'react'; -import { cn } from "../utils/cn" +import { cn } from '../utils/cn'; const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', { variants: { variant: { - default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - success: "border-transparent bg-green-500 text-white hover:bg-green-600", // Add this line + default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + success: 'border-transparent bg-green-500 text-white hover:bg-green-600', // Add this line }, }, defaultVariants: { - variant: "default", + variant: 'default', }, }, -) +); export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { - return
+ return
; } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/openframe-frontend-core/src/components/breadcrumb.tsx b/openframe-frontend-core/src/components/breadcrumb.tsx index 2e7c0025c..fca3fbcfa 100644 --- a/openframe-frontend-core/src/components/breadcrumb.tsx +++ b/openframe-frontend-core/src/components/breadcrumb.tsx @@ -1,108 +1,83 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; +import * as React from 'react'; -import { cn } from "../utils/cn" +import { cn } from '../utils/cn'; const Breadcrumb = React.forwardRef< HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode + React.ComponentPropsWithoutRef<'nav'> & { + separator?: React.ReactNode; } ->(({ ...props }, ref) =>
- + {/* Features Grid */}
{features.map((feature, index) => ( - + ))}
@@ -87,4 +77,4 @@ const OpenSourceFeatures: React.FC = () => { ); }; -export default OpenSourceFeatures; \ No newline at end of file +export default OpenSourceFeatures; diff --git a/openframe-frontend-core/src/components/open-source-icon.tsx b/openframe-frontend-core/src/components/open-source-icon.tsx index 8d0be8ab1..e0d9e318c 100644 --- a/openframe-frontend-core/src/components/open-source-icon.tsx +++ b/openframe-frontend-core/src/components/open-source-icon.tsx @@ -6,22 +6,24 @@ interface OpenSourceIconProps { height?: number; } -export const OpenSourceIcon: React.FC = ({ - className = "", - width = 19, - height = 18 -}) => { +export const OpenSourceIcon: React.FC = ({ className = '', width = 19, height = 18 }) => { return ( - - - + + ); -}; \ No newline at end of file +}; diff --git a/openframe-frontend-core/src/components/openframe-logo.tsx b/openframe-frontend-core/src/components/openframe-logo.tsx index 87bdc6eee..351be56bb 100644 --- a/openframe-frontend-core/src/components/openframe-logo.tsx +++ b/openframe-frontend-core/src/components/openframe-logo.tsx @@ -1,6 +1,11 @@ import React from 'react'; -export const OpenFrameLogo = ({ className, lowerPathColor, upperPathColor, ...props }: { className?: string, lowerPathColor?: string, upperPathColor?: string } & React.SVGProps) => { +export const OpenFrameLogo = ({ + className, + lowerPathColor, + upperPathColor, + ...props +}: { className?: string; lowerPathColor?: string; upperPathColor?: string } & React.SVGProps) => { return ( @@ -43,4 +50,4 @@ export const OpenFrameLogo = ({ className, lowerPathColor, upperPathColor, ...p ); -}; \ No newline at end of file +}; diff --git a/openframe-frontend-core/src/components/openmsp-logo.tsx b/openframe-frontend-core/src/components/openmsp-logo.tsx index 336757d5a..1e13abe12 100644 --- a/openframe-frontend-core/src/components/openmsp-logo.tsx +++ b/openframe-frontend-core/src/components/openmsp-logo.tsx @@ -8,7 +8,13 @@ interface OpenmspLogoProps extends React.SVGProps { color?: string; } -export function OpenmspLogo({ className = '', frontBubbleColor = '#000000', innerFrontBubbleColor = '#ffffff', backBubbleColor = '#ffffff', ...props }: OpenmspLogoProps) { +export function OpenmspLogo({ + className = '', + frontBubbleColor = '#000000', + innerFrontBubbleColor = '#ffffff', + backBubbleColor = '#ffffff', + ...props +}: OpenmspLogoProps) { return ( + fill={backBubbleColor} + /> + fill={innerFrontBubbleColor} + /> - + fill={frontBubbleColor} + /> + ); } diff --git a/openframe-frontend-core/src/components/pagination.tsx b/openframe-frontend-core/src/components/pagination.tsx index ddb03410f..9f8fe09b5 100644 --- a/openframe-frontend-core/src/components/pagination.tsx +++ b/openframe-frontend-core/src/components/pagination.tsx @@ -1,71 +1,71 @@ -"use client" +'use client'; -import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" -import * as React from "react" -import { cn } from "../utils/cn" -import { Button } from "./ui/button" +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; +import * as React from 'react'; +import { cn } from '../utils/cn'; +import { Button } from './ui/button'; interface PaginationProps { - currentPage: number - totalPages: number - onPageChange: (page: number) => void - className?: string + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + className?: string; } const Pagination = ({ currentPage, totalPages, onPageChange, className }: PaginationProps) => { // Generate page numbers to display const getPageNumbers = () => { - const pages = [] - const maxPagesToShow = 5 + const pages = []; + const maxPagesToShow = 5; if (totalPages <= maxPagesToShow) { // Show all pages if there are fewer than maxPagesToShow for (let i = 1; i <= totalPages; i++) { - pages.push(i) + pages.push(i); } } else { // Always show first page - pages.push(1) + pages.push(1); // Calculate start and end of page range - let start = Math.max(2, currentPage - 1) - let end = Math.min(totalPages - 1, currentPage + 1) + let start = Math.max(2, currentPage - 1); + let end = Math.min(totalPages - 1, currentPage + 1); // Adjust if at the beginning or end if (currentPage <= 2) { - end = Math.min(totalPages - 1, 4) + end = Math.min(totalPages - 1, 4); } else if (currentPage >= totalPages - 1) { - start = Math.max(2, totalPages - 3) + start = Math.max(2, totalPages - 3); } // Add ellipsis if needed if (start > 2) { - pages.push(-1) // -1 represents ellipsis + pages.push(-1); // -1 represents ellipsis } // Add middle pages for (let i = start; i <= end; i++) { - pages.push(i) + pages.push(i); } // Add ellipsis if needed if (end < totalPages - 1) { - pages.push(-2) // -2 represents ellipsis + pages.push(-2); // -2 represents ellipsis } // Always show last page if (totalPages > 1) { - pages.push(totalPages) + pages.push(totalPages); } } - return pages - } + return pages; + }; - const pageNumbers = getPageNumbers() + const pageNumbers = getPageNumbers(); return ( -