diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..9128e5e7 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [["@blac/*"]], + "access": "public", + "baseBranch": "v1", + "updateInternalDependencies": "patch", + "ignore": ["demo", "blac-docs", "perf"] +} diff --git a/.changeset/cyan-rats-vanish.md b/.changeset/cyan-rats-vanish.md new file mode 100644 index 00000000..326e5cbb --- /dev/null +++ b/.changeset/cyan-rats-vanish.md @@ -0,0 +1,8 @@ +--- +'@blac/plugin-render-logging': patch +'@blac/plugin-persistence': patch +'@blac/react': patch +'@blac/core': patch +--- + +build to js diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..53373a7f --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,16 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@blac/core": "2.0.0-rc-16", + "@blac/react": "2.0.0-rc-16", + "@blac/plugin-persistence": "2.0.0-rc-16", + "@blac/plugin-render-logging": "2.0.0-rc-16", + "demo": "1.0.0", + "blac-docs": "1.0.0", + "perf": "1.0.0" + }, + "changesets": [ + "cyan-rats-vanish" + ] +} diff --git a/.cursor/rules/base.mdc b/.cursor/rules/base.mdc deleted file mode 100644 index 5a0d598c..00000000 --- a/.cursor/rules/base.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -- Unit test are written with vitest, run tests with `pnpm run test <...>` in the package or apps directory \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..eb478a92 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [main, v1] + pull_request: + branches: [main, v1] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22.x] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.14.0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build + + - name: Run tests + run: pnpm test + + - name: Run type checks + run: pnpm typecheck + + - name: Run linter + run: pnpm lint + + - name: Check formatting + run: pnpm format --check \ No newline at end of file diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml new file mode 100644 index 00000000..a7c6d62b --- /dev/null +++ b/.github/workflows/release-manual.yml @@ -0,0 +1,69 @@ +name: Manual Release + +on: + workflow_dispatch: + inputs: + tag: + description: 'NPM tag to publish to (e.g., latest, preview, beta)' + required: true + default: 'preview' + type: choice + options: + - latest + - preview + - beta + - alpha + +permissions: + contents: write + id-token: write + +jobs: + release: + name: Manual Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.14.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build + + - name: Run tests + run: pnpm test + + - name: Run type checks + run: pnpm typecheck + + - name: Publish to npm + run: | + pnpm changeset publish --tag ${{ inputs.tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.version }} + release_name: Release v${{ steps.version.outputs.version }} + draft: false + prerelease: ${{ inputs.tag != 'latest' }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..a0640418 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + branches: + - v1 + +permissions: + contents: write + pull-requests: write + id-token: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.14.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build + + - name: Run tests + run: pnpm test + + - name: Create Release Pull Request or Publish + id: changesets + uses: changesets/action@v1 + with: + publish: pnpm release + title: 'chore: release packages' + commit: 'chore: release packages' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Notify on Release + if: steps.changesets.outputs.published == 'true' + run: | + echo "🎉 Published packages:" + echo "${{ steps.changesets.outputs.publishedPackages }}" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 59d85045..1022f40e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ packages/blac/dist apps/docs/.vitepress/cache apps/docs/.vitepress/dist +.serena diff --git a/.publish.config.json b/.publish.config.json new file mode 100644 index 00000000..3398d248 --- /dev/null +++ b/.publish.config.json @@ -0,0 +1,32 @@ +{ + "packages": { + "include": [ + "packages/*", + "packages/plugins/**" + ], + "exclude": [ + "packages/test-utils", + "packages/internal-*" + ], + "pattern": "@blac/*" + }, + "publish": { + "defaultTag": "preview", + "access": "public", + "registry": "https://registry.npmjs.org/", + "gitChecks": false + }, + "build": { + "command": "pnpm run build", + "required": true + }, + "version": { + "gitTag": false, + "allowedBumps": ["patch", "minor", "major", "prerelease"], + "syncWorkspaceDependencies": true + }, + "hooks": { + "prepublish": "", + "postpublish": "" + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..28c439d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,269 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +# BlaC State Management Library + +A sophisticated TypeScript state management library implementing the BLoC (Business Logic Component) pattern with innovative proxy-based dependency tracking for JavaScript/TypeScript applications. + +## Project Overview + +BlaC is a monorepo containing: +- **Core state management library** (`@blac/core`) with reactive Bloc/Cubit pattern +- **React integration** (`@blac/react`) with optimized hooks and dependency tracking +- **Plugin ecosystem** for persistence, logging, and extensibility +- **Demo applications** showcasing patterns and usage +- **Comprehensive documentation** and examples + +## Architecture + +### Core Concepts +- **Blocs**: Event-driven state containers using class-based event handlers +- **Cubits**: Simple state containers with direct state emission +- **Proxy-based dependency tracking**: Automatic optimization of React re-renders +- **Plugin system**: Extensible architecture for custom functionality +- **Instance management**: Shared, isolated, and persistent state patterns + +### Key Design Patterns +- Arrow function methods required for proper `this` binding in React +- Type-safe event handling with class-based events +- Automatic memory management with WeakRef-based consumer tracking +- Configuration-driven behavior (proxy tracking, logging, etc.) + +## Development Setup + +### Prerequisites +- Node.js 22+ +- pnpm 9+ + +### Installation +```bash +pnpm install +``` + +### Key Commands +- **Development**: `pnpm dev` - Start all demo apps in parallel +- **Build**: `pnpm build` - Build all packages +- **Test**: `pnpm test` - Run all tests +- **Test (watch)**: `pnpm test:watch` - Run tests in watch mode +- **Lint**: `pnpm lint` - Run ESLint across all packages +- **Type check**: `pnpm typecheck` - Verify TypeScript types +- **Format**: `pnpm format` - Format code with Prettier + +### Project Structure +``` +/ +├── packages/ +│ ├── blac/ # Core state management (@blac/core) +│ ├── blac-react/ # React integration (@blac/react) +│ ├── plugin-render-logging/ # Render logging plugin +│ └── plugins/ +│ └── bloc/ +│ └── persistence/ # Persistence plugin +├── apps/ +│ ├── demo/ # Main demo application +│ ├── docs/ # Documentation site +│ └── perf/ # Performance testing app +├── turbo.json # Turbo build configuration +├── pnpm-workspace.yaml # pnpm workspace configuration +└── tsconfig.base.json # Base TypeScript configuration +``` + +## Code Conventions + +### Critical Requirements +1. **Arrow Functions**: All methods in Bloc/Cubit classes MUST use arrow function syntax: + ```typescript + // ✅ Correct + increment = () => { + this.emit(this.state + 1); + }; + + // ❌ Incorrect - will break in React + increment() { + this.emit(this.state + 1); + } + ``` + +2. **Type Safety**: Prefer explicit types over `any`, use proper generic constraints +3. **Event Classes**: Use class-based events for Blocs, not plain objects +4. **State Immutability**: Always emit new state objects, never mutate existing state + +### Code Style +- **TypeScript**: Strict mode enabled with comprehensive type checking +- **ESLint**: Strict TypeScript rules enforced +- **Prettier**: Automatic code formatting +- **Testing**: Comprehensive test coverage with Vitest + +## State Management Patterns + +### Basic Cubit Example +```typescript +class CounterCubit extends Cubit { + constructor() { + super(0); // Initial state + } + + increment = () => { + this.emit(this.state + 1); + }; + + decrement = () => { + this.emit(this.state - 1); + }; +} +``` + +### Event-Driven Bloc Example +```typescript +// Define event classes +class IncrementEvent { + constructor(public readonly amount: number = 1) {} +} + +class CounterBloc extends Bloc { + constructor() { + super(0); + + this.on(IncrementEvent, (event, emit) => { + emit(this.state + event.amount); + }); + } + + increment = (amount = 1) => { + this.add(new IncrementEvent(amount)); + }; +} +``` + +### React Integration +```typescript +function Counter() { + const [state, counterBloc] = useBloc(CounterBloc); + + return ( +
+

Count: {state.count}

+ +
+ ); +} +``` + +## Configuration & Features + +### Global Configuration +```typescript +import { Blac } from '@blac/core'; + +Blac.setConfig({ + proxyDependencyTracking: true, // Automatic dependency tracking +}); +``` + +### Instance Management Patterns +- **Shared (default)**: Single instance across all consumers +- **Isolated**: Each consumer gets its own instance (`static isolated = true`) +- **Persistent**: Keep alive even without consumers (`static keepAlive = true`) + +### Plugin System +```typescript +class LoggerPlugin implements BlacPlugin { + name = 'LoggerPlugin'; + + onEvent(event: BlacLifecycleEvent, bloc: BlocBase, params?: any) { + if (event === BlacLifecycleEvent.STATE_CHANGED) { + console.log(`[${bloc._name}] State changed:`, bloc.state); + } + } +} + +Blac.addPlugin(new LoggerPlugin()); +``` + +## Performance Considerations + +- **Proxy overhead**: Automatic dependency tracking uses Proxies (can be disabled) +- **Consumer validation**: O(n) consumer cleanup on state changes +- **Memory management**: WeakRef-based consumer tracking prevents memory leaks +- **Batched updates**: State changes are batched for performance + +## Common Patterns + +### Conditional Dependencies +```typescript +const [state, bloc] = useBloc(UserBloc, { + selector: (currentState, previousState, instance) => [ + currentState.isLoggedIn ? currentState.userData : null, + instance.computedValue + ] +}); +``` + +### Props-Based Blocs +```typescript +class UserProfileBloc extends Bloc { + constructor(props: {userId: string}) { + super(initialState); + this.userId = props.userId; + this._name = `UserProfileBloc_${this.userId}`; + } +} +``` + +## Known Issues & Limitations + +### Current Architecture Challenges +- Circular dependencies between core classes +- Complex dual consumer/observer subscription system +- Performance bottlenecks with deep object proxies +- Security considerations with global state access + +### Recommended Improvements +- Simplify subscription architecture +- Implement reference counting for lifecycle management +- Add selector-based optimization options +- Enhance type safety throughout + +## Building & Deployment + +### Local Development +```bash +# Start demo app +cd apps/demo && pnpm dev + +# Run specific package tests +cd packages/blac && pnpm test + +# Build specific package +cd packages/blac && pnpm build +``` + +### Publishing +```bash +# Build and publish core package +cd packages/blac && pnpm deploy + +# Build and publish React package +cd packages/blac-react && pnpm deploy +``` + +## Additional Resources + +- **Documentation**: `/apps/docs/` contains comprehensive guides +- **Examples**: `/apps/demo/` showcases all major patterns +- **Performance**: `/apps/perf/` for performance testing +- **Architecture Review**: `/blac-improvements.md` contains detailed improvement proposals +- **Code Review**: `/review.md` contains comprehensive codebase analysis + +## Contributing + +1. Follow arrow function convention for all Bloc/Cubit methods +2. Write comprehensive tests for new features +3. Update documentation for public API changes +4. Run full test suite before submitting PRs +5. Follow existing TypeScript strict mode conventions + +--- + +*This codebase implements advanced state management patterns with sophisticated dependency tracking. Pay special attention to the arrow function requirement and proxy-based optimizations when working with the code.* diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 5a3dc727..00000000 --- a/TODO.md +++ /dev/null @@ -1,76 +0,0 @@ -# Blac Framework TODO - -This document tracks features that are described in the documentation but require implementation or enhancement in the actual packages. - -## High Priority Features - -### Event Transformation in Bloc -**Status:** Documented but needs implementation -**Reasoning:** Event transformation enables critical reactive patterns like debouncing and filtering events before they're processed, essential for search inputs and form validation to prevent unnecessary API calls and improve performance. - -```typescript -// As shown in docs -this.transform(SearchQueryChanged, events => - events.pipe( - debounceTime(300), - distinctUntilChanged((prev, curr) => prev.query === curr.query) - ) -); -``` - -### Concurrent Event Processing -**Status:** Documented but needs implementation -**Reasoning:** Allows non-dependent events to be processed simultaneously, significantly improving performance for apps with many parallel operations like data fetching from multiple sources. - -```typescript -// As shown in docs -this.concurrentEventProcessing = true; -``` - -## Medium Priority Features - -### `patch()` Method Enhancements -**Status:** Basic implementation exists, needs enhancement -**Reasoning:** The current implementation needs better handling of nested objects and arrays to reduce boilerplate when updating complex state structures. - -```typescript -// Current usage that's verbose for nested updates -this.patch({ - loadingState: { - ...this.state.loadingState, - isInitialLoading: false - } -}); - -// Desired usage with path-based updates -this.patch('loadingState.isInitialLoading', false); -``` - -### Improved Instance Management -**Status:** Basic support exists, needs enhancement -**Reasoning:** The `isolated` and `keepAlive` static properties need more robust lifecycle management and memory optimization to prevent memory leaks in large applications. - -## Low Priority Features - -### TypeSafe Events -**Status:** Documented but basic implementation -**Reasoning:** Event type checking could be improved to provide compile-time safety when dispatching events to the appropriate handlers. - -```typescript -// Goal: Make this relationship type-safe at compile time -this.on(IncrementPressed, this.increment); -``` - -### Enhanced Debugging Tools -**Status:** Minimally implemented -**Reasoning:** Developer tools for inspecting bloc state transitions and event flows would make debugging complex state management issues much easier, especially for larger applications. - -## Future Considerations - -### Server-side Bloc Support -**Status:** Not implemented -**Reasoning:** Supporting SSR with Blac would improve SEO and initial load performance for Next.js and other SSR frameworks. - -### Persistence Adapters -**Status:** Not implemented -**Reasoning:** Built-in support for persisting bloc state to localStorage, sessionStorage, or IndexedDB would greatly simplify offline-first app development. \ No newline at end of file diff --git a/apps/docs/.eslintignore b/apps/docs/.eslintignore deleted file mode 100644 index 4541f2fb..00000000 --- a/apps/docs/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -.vitepress/theme/mermaid-theme.js -.vitepress/theme/index.js \ No newline at end of file diff --git a/apps/docs/.prettierignore b/apps/docs/.prettierignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/apps/docs/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts index 739931c0..a53ab94a 100644 --- a/apps/docs/.vitepress/config.mts +++ b/apps/docs/.vitepress/config.mts @@ -1,128 +1,244 @@ import { defineConfig } from 'vitepress'; -import { withMermaid } from "vitepress-plugin-mermaid"; - +import { withMermaid } from 'vitepress-plugin-mermaid'; // https://vitepress.dev/reference/site-config const siteConfig = defineConfig({ - title: "Blac Documentation", - description: "Lightweight, flexible state management for React applications with predictable data flow", + title: 'BlaC', + description: + 'Business Logic as Components - Simple, powerful state management that separates business logic from UI. Type-safe, testable, and scales with your React application.', head: [ - ['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }] + ['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }], + ['meta', { name: 'theme-color', content: '#61dafb' }], + ['meta', { name: 'og:type', content: 'website' }], + ['meta', { name: 'og:site_name', content: 'BlaC' }], + ['meta', { name: 'og:image', content: '/logo.svg' }], + ['meta', { name: 'twitter:card', content: 'summary' }], + ['meta', { name: 'twitter:image', content: '/logo.svg' }], ], themeConfig: { - // https://vitepress.dev/reference/default-theme-config logo: '/logo.svg', + siteTitle: 'BlaC', + nav: [ - { text: 'Home', link: '/' }, - { text: 'Learn', link: '/learn/introduction' }, - { text: 'Reference', link: '/reference/core-classes' }, - { text: 'Examples', link: '/examples/counter' } + { text: 'Guide', link: '/introduction' }, + { text: 'API', link: '/api/core/blac' }, + { text: 'GitHub', link: 'https://github.com/jsnanigans/blac' }, ], sidebar: [ { text: 'Introduction', items: [ - { text: 'Introduction', link: '/learn/introduction' }, - { text: 'Getting Started', link: '/learn/getting-started' }, - { text: 'Architecture', link: '/learn/architecture' }, - { text: 'Core Concepts', link: '/learn/core-concepts' } - ] + { text: 'What is BlaC?', link: '/introduction' }, + { text: 'Comparisons', link: '/comparisons' }, + ], }, { - text: 'Basic Usage', + text: 'Getting Started', items: [ - { text: 'The Blac Pattern', link: '/learn/blac-pattern' }, - { text: 'State Management Patterns', link: '/learn/state-management-patterns' }, - { text: 'Best Practices', link: '/learn/best-practices' } - ] + { text: 'Learning Paths', link: '/getting-started/learning-paths' }, + { text: 'Installation', link: '/getting-started/installation' }, + { text: 'Your First Cubit', link: '/getting-started/first-cubit' }, + { + text: 'Async Operations', + link: '/getting-started/async-operations', + }, + { text: 'Your First Bloc', link: '/getting-started/first-bloc' }, + ], + }, + { + text: 'Core Concepts', + items: [ + { text: 'State Management', link: '/concepts/state-management' }, + { text: 'Cubits', link: '/concepts/cubits' }, + { text: 'Blocs', link: '/concepts/blocs' }, + { + text: 'Instance Management', + link: '/concepts/instance-management', + }, + ], }, { text: 'API Reference', items: [ - { text: 'Core Classes', link: '/api/core-classes' }, - { text: 'React Hooks', link: '/api/react-hooks' }, - { text: 'Key Methods', link: '/api/key-methods' } - ] + { + text: '@blac/core', + collapsed: false, + items: [ + { text: 'Blac', link: '/api/core/blac' }, + { text: 'Cubit', link: '/api/core/cubit' }, + { text: 'Bloc', link: '/api/core/bloc' }, + { text: 'BlocBase', link: '/api/core/bloc-base' }, + ], + }, + { + text: '@blac/react', + collapsed: false, + items: [ + { text: 'useBloc', link: '/api/react/use-bloc' }, + { + text: 'useExternalBlocStore', + link: '/api/react/use-external-bloc-store', + }, + ], + }, + ], + }, + { + text: 'React Integration', + items: [ + { text: 'Hooks', link: '/react/hooks' }, + { text: 'Patterns', link: '/react/patterns' }, + ], + }, + { + text: 'Plugin System', + items: [ + { text: 'Overview', link: '/plugins/overview' }, + { text: 'System Plugins', link: '/plugins/system-plugins' }, + { text: 'Bloc Plugins', link: '/plugins/bloc-plugins' }, + { text: 'Creating Plugins', link: '/plugins/creating-plugins' }, + { text: 'Persistence Plugin', link: '/plugins/persistence' }, + { text: 'API Reference', link: '/plugins/api-reference' }, + ], + }, + { + text: 'Patterns & Recipes', + collapsed: true, + items: [ + { text: 'Testing', link: '/patterns/testing' }, + { text: 'Persistence', link: '/patterns/persistence' }, + { text: 'Error Handling', link: '/patterns/error-handling' }, + { text: 'Performance', link: '/patterns/performance' }, + ], + }, + { + text: 'Examples', + items: [ + { text: 'Counter', link: '/examples/counter' }, + { text: 'Todo List', link: '/examples/todo-list' }, + { text: 'Authentication', link: '/examples/authentication' }, + ], + }, + { + text: 'Legacy', + collapsed: true, + items: [ + { text: 'Old Introduction', link: '/learn/introduction' }, + { text: 'Old Getting Started', link: '/learn/getting-started' }, + { text: 'Old Architecture', link: '/learn/architecture' }, + { text: 'Old Core Concepts', link: '/learn/core-concepts' }, + { text: 'The Blac Pattern', link: '/learn/blac-pattern' }, + { + text: 'State Management Patterns', + link: '/learn/state-management-patterns', + }, + { text: 'Best Practices', link: '/learn/best-practices' }, + { text: 'Old Core Classes', link: '/api/core-classes' }, + { text: 'Old React Hooks', link: '/api/react-hooks' }, + { text: 'Key Methods', link: '/api/key-methods' }, + { text: 'Configuration', link: '/api/configuration' }, + ], }, - // { - // text: 'Examples', - // items: [ - // ] - // } ], socialLinks: [ - { icon: 'github', link: 'https://github.com/jsnanigans/blac' } + { icon: 'github', link: 'https://github.com/jsnanigans/blac' }, ], search: { - provider: 'local' + provider: 'local', + options: { + detailedView: true, + }, }, footer: { message: 'Released under the MIT License.', - copyright: 'Copyright © 2023-present Blac Contributors' + copyright: 'Copyright © 2023-present BlaC Contributors', + }, + + editLink: { + pattern: 'https://github.com/jsnanigans/blac/edit/main/apps/docs/:path', + text: 'Edit this page on GitHub', + }, + + lastUpdated: { + text: 'Updated at', + formatOptions: { + dateStyle: 'short', + timeStyle: 'medium', + }, + }, + }, + + markdown: { + theme: { + light: 'github-light', + dark: 'github-dark', }, }, - // The mermaidPlugin block that was previously here has been removed - // and is now handled by the withMermaid wrapper. }); export default withMermaid({ - ...siteConfig, // Spread the base VitePress configuration + ...siteConfig, - // MermaidConfig - for mermaid.js core options + // Mermaid configuration mermaid: { - // refer https://mermaid.js.org/config/setup/modules/mermaidAPI.html#mermaidapi-configuration-defaults for options - // Add future Mermaid core configurations here (e.g., theme, securityLevel) - theme: 'base', // We use 'base' and override variables for a custom neon look + theme: 'base', themeVariables: { - // --- Core Colors for Neon Look --- - primaryColor: '#00BFFF', // DeepSkyBlue (Vibrant Blue for main nodes) - primaryTextColor: '#FFFFFF', // White text on nodes - primaryBorderColor: '#FF00FF', // Neon Magenta (for borders, "cute" pop) - - lineColor: '#39FF14', // Neon Green (for arrows, connectors) - textColor: '#E0E0E0', // Light Grey for general text/labels - - // --- Backgrounds --- - mainBkg: '#1A1A1A', // Very Dark Grey (to make neons pop) - clusterBkg: '#242424', // Dark Grey Soft (for subgraphs) - clusterBorderColor: '#00BFFF', // DeepSkyBlue border for clusters - - // --- Node specific (if not covered by primary) --- - // These often inherit from primary, but can be set explicitly - nodeBorder: '#FF00FF', // Consistent with primaryBorderColor (Neon Magenta) - nodeTextColor: '#FFFFFF', // Consistent with primaryTextColor (White) - - // --- Accents & Special Elements: "Cute Neon" Notes --- - noteBkgColor: '#2c003e', // Deep Dark Magenta/Purple base for notes - noteTextColor: '#FFFFA0', // Neon Pale Yellow text on notes (cute & readable) - noteBorderColor: '#FF00AA', // Bright Neon Pink border for notes - - // --- For Sequence Diagrams (Neon) --- - actorBkg: '#39FF14', // Neon Green for actor boxes - actorBorder: '#2E8B57', // SeaGreen (darker green border for actors for definition) - actorTextColor: '#000000', // Black text on Neon Green actors for contrast - - signalColor: '#FF00FF', // Neon Magenta for signal lines - signalTextColor: '#FFFFFF', // White text on signal lines - - labelBoxBkgColor: '#BF00FF', // Electric Purple for label boxes (like 'loop', 'alt') - labelTextColor: '#FFFFFF', // White text on label boxes - - sequenceNumberColor: '#FFFFFF', // White for sequence numbers for visibility on dark lanes - - // --- Fonts - aligning with common VitePress/modern web defaults --- - fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif', - fontSize: '22px', // Standard readable size - } + // Clean, minimal theme + primaryColor: '#61dafb', + primaryTextColor: '#fff', + primaryBorderColor: '#4db8d5', + lineColor: '#5e6c84', + secondaryColor: '#f4f5f7', + tertiaryColor: '#e3e4e6', + background: '#ffffff', + mainBkg: '#61dafb', + secondBkg: '#f4f5f7', + tertiaryBkg: '#e3e4e6', + secondaryBorderColor: '#c1c7d0', + tertiaryBorderColor: '#d3d5d9', + secondaryTextColor: '#172b4d', + tertiaryTextColor: '#42526e', + textColor: '#172b4d', + mainContrastColor: '#172b4d', + darkTextColor: '#172b4d', + border1: '#4db8d5', + border2: '#c1c7d0', + arrowheadColor: '#5e6c84', + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontSize: '16px', + labelBackground: '#f4f5f7', + nodeBkg: '#61dafb', + nodeBorder: '#4db8d5', + clusterBkg: '#f4f5f7', + clusterBorder: '#c1c7d0', + defaultLinkColor: '#5e6c84', + titleColor: '#172b4d', + edgeLabelBackground: '#ffffff', + actorBorder: '#4db8d5', + actorBkg: '#61dafb', + actorTextColor: '#ffffff', + actorLineColor: '#5e6c84', + signalColor: '#172b4d', + signalTextColor: '#172b4d', + labelBoxBkgColor: '#61dafb', + labelBoxBorderColor: '#4db8d5', + labelTextColor: '#ffffff', + loopTextColor: '#172b4d', + noteBorderColor: '#c1c7d0', + noteBkgColor: '#fff8dc', + noteTextColor: '#172b4d', + activationBorderColor: '#172b4d', + activationBkgColor: '#f4f5f7', + sequenceNumberColor: '#ffffff', + }, }, - // MermaidPluginConfig - for the vitepress-plugin-mermaid itself mermaidPlugin: { - class: "mermaid my-class", // existing setting: set additional css classes for parent container - // Add other vitepress-plugin-mermaid specific options here if needed + class: 'mermaid', }, }); diff --git a/apps/docs/.vitepress/theme/NotFound.vue b/apps/docs/.vitepress/theme/NotFound.vue index da95696f..0d531141 100644 --- a/apps/docs/.vitepress/theme/NotFound.vue +++ b/apps/docs/.vitepress/theme/NotFound.vue @@ -49,7 +49,11 @@ h1 { height: 2px; width: 60px; margin: 24px auto; - background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-accent)); + background: linear-gradient( + 90deg, + var(--vp-c-brand), + var(--vp-c-brand-accent) + ); } h2 { @@ -102,4 +106,4 @@ p { gap: 12px; } } - \ No newline at end of file + diff --git a/apps/docs/.vitepress/theme/custom-home.css b/apps/docs/.vitepress/theme/custom-home.css index 406770c7..34c6bc02 100644 --- a/apps/docs/.vitepress/theme/custom-home.css +++ b/apps/docs/.vitepress/theme/custom-home.css @@ -1,108 +1,51 @@ -/* Custom Home Page Styling */ -.home-content { - padding: 2rem 0; -} - -.tagline { - font-size: 1.5rem; - color: var(--vp-c-text-2); - margin-bottom: 2rem; - text-align: center; -} - -.image-container { - display: flex; - justify-content: center; - margin: 3rem 0; -} - -.image-container img { - width: 180px; - height: auto; - transition: transform 0.5s ease; -} - -.image-container img:hover { - transform: rotate(10deg) scale(1.1); -} +/* Custom styles for the home page */ -.actions { - display: flex; - justify-content: center; - gap: 1rem; - margin: 2rem 0; +/* Hero section */ +.VPHero { + padding: 48px 24px !important; } -.action { - display: inline-block; - padding: 0.75rem 1.5rem; - font-weight: 500; - background-color: var(--vp-c-brand); - color: white; - border-radius: 4px; - transition: background-color 0.2s ease; - text-decoration: none; +.VPHomeHero { + margin: 0 auto; + padding: 48px 24px; } -.action:hover { - background-color: var(--vp-c-brand-light); +/* Features section */ +.VPHomeFeatures { + padding: 24px; } -.action.alt { - background-color: var(--vp-c-bg-soft); - color: var(--vp-c-text-1); - border: 1px solid var(--vp-c-divider); +.VPFeatures { + padding: 0 24px; } -.action.alt:hover { - border-color: var(--vp-c-brand); - color: var(--vp-c-brand); +/* Feature cards */ +.VPFeature { + border-radius: 12px; + padding: 24px; } -.features { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin: 3rem 0; -} - -.feature { - padding: 1.5rem; - border-radius: 8px; - background-color: var(--vp-c-bg-soft); - transition: transform 0.2s ease, box-shadow 0.2s ease; - border: 1px solid var(--vp-c-divider); -} - -.feature:hover { - transform: translateY(-4px); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); -} +/* Responsive adjustments */ +@media (min-width: 640px) { + .VPHero { + padding: 64px 48px !important; + } -.feature h3 { - margin-top: 0; - color: var(--vp-c-brand); - font-size: 1.3rem; -} + .VPHomeHero { + padding: 64px 48px; + } -/* Dark mode adjustments */ -.dark .action.alt { - background-color: rgba(255, 255, 255, 0.1); + .VPHomeFeatures { + padding: 48px; + } } -/* Responsive adjustments */ -@media (max-width: 640px) { - .features { - grid-template-columns: 1fr; - } - - .actions { - flex-direction: column; - align-items: center; +@media (min-width: 960px) { + .VPHero { + padding: 96px 64px !important; } - - .action { - width: 80%; - text-align: center; + + .VPHomeHero { + padding: 96px 64px; } -} \ No newline at end of file +} diff --git a/apps/docs/.vitepress/theme/custom.css b/apps/docs/.vitepress/theme/custom.css index 5c70821d..bf23445f 100644 --- a/apps/docs/.vitepress/theme/custom.css +++ b/apps/docs/.vitepress/theme/custom.css @@ -1,69 +1,382 @@ +/** + * Customize default theme styling by overriding CSS variables + */ + :root { - --vp-c-brand: #61dafb; - --vp-c-brand-light: #7de3ff; - --vp-c-brand-lighter: #9eeaff; - --vp-c-brand-dark: #4db8d5; - --vp-c-brand-darker: #3990a8; - - --vp-c-brand-accent: #ff00ff; - - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: linear-gradient( - 120deg, - #61dafb 30%, - #ff00ff - ); - - --vp-c-bg-alt: #f9f9f9; - --vp-c-bg-soft: #f3f3f3; + /* Brand Colors */ + --vp-c-brand-1: #61dafb; + --vp-c-brand-2: #4db8d5; + --vp-c-brand-3: #3990a8; + --vp-c-brand-soft: rgba(97, 218, 251, 0.14); + + /* Override theme colors */ + --vp-c-default-1: #515c67; + --vp-c-default-2: #414853; + --vp-c-default-3: #32383f; + --vp-c-default-soft: rgba(101, 117, 133, 0.16); + + /* Text Colors */ + --vp-c-text-1: var(--vp-c-brand-darker); + --vp-c-text-2: #3c4349; + --vp-c-text-3: #8b9aa8; + + /* Background Colors */ + --vp-c-bg: #ffffff; + --vp-c-bg-soft: #f9fafb; + --vp-c-bg-mute: #f3f4f6; + --vp-c-bg-alt: #f9fafb; + + /* Typography */ + --vp-font-family-base: + Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + --vp-font-family-mono: + 'JetBrains Mono', source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } .dark { - --vp-c-bg-alt: #1a1a1a; - --vp-c-bg-soft: #242424; + /* Dark mode overrides */ + --vp-c-bg: #0d1117; + --vp-c-bg-soft: #161b22; + --vp-c-bg-mute: #1f2428; + --vp-c-bg-alt: #161b22; + + --vp-c-text-1: #e6edf3; + --vp-c-text-2: #b1bac4; + --vp-c-text-3: #8b949e; + + --vp-c-default-soft: rgba(101, 117, 133, 0.16); } -/* Code block styling */ -html.dark .vp-doc div[class*='language-'] { - background-color: #1a1a1a; +/** + * Component: Custom Block + */ +.custom-block { + border-radius: 8px; } -/* Logo animation */ -.VPNavBarTitle .logo { - transition: transform 0.3s ease; +.custom-block.tip { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-brand-soft); } -.VPNavBarTitle:hover .logo { - transform: rotate(10deg); +.custom-block.warning { + border-color: #f59e0b; + background-color: rgba(245, 158, 11, 0.12); } -/* Button styling */ -.VPButton.brand { - border-color: var(--vp-c-brand); - color: white; - background-color: var(--vp-c-brand); +.custom-block.danger { + border-color: #ef4444; + background-color: rgba(239, 68, 68, 0.12); } -.VPButton.brand:hover { - border-color: var(--vp-c-brand-light); - background-color: var(--vp-c-brand-light); +.custom-block.info { + border-color: #3b82f6; + background-color: rgba(59, 130, 246, 0.12); } -/* Custom block styling */ -.custom-block.tip { - border-color: var(--vp-c-brand); +/** + * Component: Code + */ +.vp-doc div[class*='language-'] { + border-radius: 8px; + margin: 16px 0; } -.custom-block.warning { - border-color: var(--vp-c-brand-accent); +.vp-doc div[class*='language-'] code { + font-size: 14px; + line-height: 1.5; +} + +/* Inline code */ +.vp-doc :not(pre) > code { + color: var(--vp-c-brand-1); + background-color: var(--vp-c-default-soft); + padding: 0.25rem 0.375rem; + border-radius: 4px; + font-size: 0.875em; +} + +/** + * Component: Button + */ +.vp-button { + border-radius: 24px; + font-weight: 500; + transition: all 0.25s; +} + +.vp-button:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/** + * Component: Home + */ +.VPHome { + padding-top: 0 !important; +} + +.VPHomeHero { + padding: 48px 24px !important; +} + +.VPHomeHero .name { + font-size: 48px !important; + font-weight: 700; + letter-spacing: -0.02em; +} + +.VPHomeHero .text { + font-size: 24px !important; + font-weight: 500; + margin: 8px 0 16px !important; +} + +.VPHomeHero .tagline { + font-size: 20px !important; + color: var(--vp-c-text-2); + font-weight: 400; + margin: 0 0 32px !important; +} + +/** + * Component: Features + */ +.VPFeatures { + padding: 32px 24px 0 !important; } -/* Feature section */ .VPFeature { - transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + padding: 24px !important; + background-color: var(--vp-c-bg-soft); + transition: all 0.25s; } .VPFeature:hover { + border-color: var(--vp-c-brand-1); transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} \ No newline at end of file + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.VPFeature .icon { + width: 48px; + height: 48px; + font-size: 32px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; +} + +.VPFeature .title { + font-size: 18px; + font-weight: 600; + margin-bottom: 8px; +} + +.VPFeature .details { + font-size: 14px; + color: var(--vp-c-text-2); + line-height: 1.6; +} + +/** + * Component: Sidebar + */ +.VPSidebar { + padding-top: 24px; +} + +.VPSidebarItem { + margin: 0; +} + +.VPSidebarItem .text { + font-size: 14px; + transition: color 0.25s; +} + +.VPSidebarItem.level-0 > .item > .text { + font-weight: 600; + color: var(--vp-c-text-1); +} + +.VPSidebarItem .link:hover .text { + color: var(--vp-c-brand-1); +} + +.VPSidebarItem .link.active .text { + color: var(--vp-c-brand-1); + font-weight: 500; +} + +/** + * Component: Nav + */ +.VPNavBar { + background-color: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--vp-c-divider); +} + +.dark .VPNavBar { + background-color: rgba(13, 17, 23, 0.8); +} + +.VPNavBarTitle .title { + font-weight: 600; + font-size: 18px; +} + +/** + * Component: Tables + */ +.vp-doc table { + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--vp-c-divider); +} + +.vp-doc tr { + border-top: 1px solid var(--vp-c-divider); +} + +.vp-doc th { + background-color: var(--vp-c-bg-soft); + font-weight: 600; +} + +.vp-doc td { + padding: 12px 16px; +} + +/** + * Component: Doc Footer + */ +.VPDocFooter { + margin-top: 64px; + padding-top: 32px; + border-top: 1px solid var(--vp-c-divider); +} + +/** + * Utilities + */ +.vp-doc h1 { + font-size: 32px; + font-weight: 700; + margin: 32px 0 16px; + letter-spacing: -0.02em; +} + +.vp-doc h2 { + font-size: 24px; + font-weight: 600; + margin: 48px 0 16px; + padding-top: 24px; + letter-spacing: -0.02em; + border-top: 1px solid var(--vp-c-divider); +} + +.vp-doc h2:first-child { + border-top: none; + padding-top: 0; + margin-top: 0; +} + +.vp-doc h3 { + font-size: 20px; + font-weight: 600; + margin: 32px 0 16px; + letter-spacing: -0.01em; +} + +.vp-doc p { + line-height: 1.7; + margin: 16px 0; +} + +.vp-doc ul, +.vp-doc ol { + margin: 16px 0; + padding-left: 24px; +} + +.vp-doc li { + margin: 8px 0; + line-height: 1.7; +} + +/* Blockquotes */ +.vp-doc blockquote { + border-left: 4px solid var(--vp-c-brand-1); + padding-left: 16px; + margin: 24px 0; + color: var(--vp-c-text-2); + font-style: italic; +} + +/* Details/Summary */ +.vp-doc details { + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + padding: 16px; + margin: 16px 0; + background-color: var(--vp-c-bg-soft); +} + +.vp-doc summary { + cursor: pointer; + font-weight: 600; + margin: -16px -16px 0; + padding: 16px; + background-color: var(--vp-c-bg-mute); + border-radius: 8px 8px 0 0; +} + +.vp-doc details[open] summary { + margin-bottom: 16px; +} + +/* Horizontal Rule */ +.vp-doc hr { + border: none; + border-top: 1px solid var(--vp-c-divider); + margin: 48px 0; +} + +/* Focus styles */ +:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 2px; +} + +/* Selection */ +::selection { + background-color: var(--vp-c-brand-soft); + color: var(--vp-c-text-1); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--vp-c-bg-soft); +} + +::-webkit-scrollbar-thumb { + background: var(--vp-c-default-3); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--vp-c-default-2); +} diff --git a/apps/docs/.vitepress/theme/env.d.ts b/apps/docs/.vitepress/theme/env.d.ts index 0743a095..048f176f 100644 --- a/apps/docs/.vitepress/theme/env.d.ts +++ b/apps/docs/.vitepress/theme/env.d.ts @@ -1,7 +1,7 @@ /// declare module '*.vue' { - import type { DefineComponent } from 'vue' - const component: DefineComponent<{}, {}, any> - export default component -} \ No newline at end of file + import type { DefineComponent } from 'vue'; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/apps/docs/.vitepress/theme/index.ts b/apps/docs/.vitepress/theme/index.ts index 6d5cface..19eb2b57 100644 --- a/apps/docs/.vitepress/theme/index.ts +++ b/apps/docs/.vitepress/theme/index.ts @@ -1,15 +1,19 @@ import DefaultTheme from 'vitepress/theme'; import { h } from 'vue'; import NotFound from './NotFound.vue'; -import './custom-home.css'; import './custom.css'; +import './custom-home.css'; +import './style.css'; export default { extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // You can add custom slots here if needed - }) + }); }, NotFound, -} \ No newline at end of file + enhanceApp({ app }) { + // You can register global components here if needed + }, +}; diff --git a/apps/docs/.vitepress/theme/style.css b/apps/docs/.vitepress/theme/style.css new file mode 100644 index 00000000..0fe6b5c0 --- /dev/null +++ b/apps/docs/.vitepress/theme/style.css @@ -0,0 +1,465 @@ +/** + * BlaC Documentation Theme + * Minimal, clean design focused on content + */ + +/* ======================================== + Custom Properties + ======================================== */ + +:root { + /* Brand Colors */ + --blac-cyan: #61dafb; + --blac-cyan-dark: #4db8d5; + --blac-cyan-darker: #3990a8; + --blac-cyan-darkest: #2a6e86; /* Even darker for better contrast */ + --blac-cyan-soft: rgba(97, 218, 251, 0.1); + + /* Override VitePress defaults */ + --vp-c-brand-1: var(--blac-cyan); + --vp-c-brand-2: var(--blac-cyan-dark); + --vp-c-brand-3: var(--blac-cyan-darker); + --vp-c-brand-soft: var(--blac-cyan-soft); + + /* Typography */ + --vp-font-family-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --vp-font-family-mono: + ui-monospace, SFMono-Regular, 'SF Mono', Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + + /* Spacing */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 3rem; + --space-3xl: 4rem; + + /* Borders */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} + +/* ======================================== + Base Styles + ======================================== */ + +/* Smooth transitions */ +* { + transition-property: + color, background-color, border-color, box-shadow, transform; + transition-duration: 200ms; + transition-timing-function: ease; +} + +/* Better focus styles */ +*:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 2px; +} + +/* ======================================== + Typography + ======================================== */ + +.vp-doc h1 { + font-size: 2.5rem; + line-height: 1.2; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: var(--space-xl); +} + +.vp-doc h2 { + font-size: 1.875rem; + line-height: 1.3; + font-weight: 700; + letter-spacing: -0.01em; + margin-top: var(--space-3xl); + margin-bottom: var(--space-lg); + padding-top: var(--space-lg); + border-top: 1px solid var(--vp-c-divider); +} + +.vp-doc h2:first-child { + border-top: none; + margin-top: 0; + padding-top: 0; +} + +.vp-doc h3 { + font-size: 1.375rem; + line-height: 1.4; + font-weight: 600; + margin-top: var(--space-2xl); + margin-bottom: var(--space-md); +} + +.vp-doc p { + line-height: 1.7; + margin-bottom: var(--space-md); +} + +/* Lead paragraph */ +.vp-doc h1 + p { + font-size: 1.125rem; + color: var(--vp-c-text-2); + line-height: 1.7; + margin-bottom: var(--space-xl); +} + +/* ======================================== + Code Blocks + ======================================== */ + +.vp-doc div[class*='language-'] { + border-radius: var(--radius-lg); + margin: var(--space-lg) 0; + background-color: var(--vp-code-block-bg); + border: 1px solid var(--vp-c-divider); + overflow: hidden; +} + +.vp-doc div[class*='language-'] code { + font-size: 0.875rem; + line-height: 1.6; + font-family: var(--vp-font-family-mono); +} + +/* Inline code */ +.vp-doc :not(pre) > code { + background-color: var(--vp-c-brand-soft); + color: var(--vp-c-brand-darker); + padding: 0.125rem 0.375rem; + border-radius: var(--radius-sm); + font-size: 0.875em; + font-weight: 500; + border: 1px solid var(--vp-c-brand-2); +} + +.dark .vp-doc :not(pre) > code { + background-color: rgba(97, 218, 251, 0.15); + color: var(--vp-c-brand-1); + border-color: rgba(97, 218, 251, 0.3); +} + +/* ======================================== + Tables + ======================================== */ + +.vp-doc table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: var(--space-xl) 0; + border: 1px solid var(--vp-c-divider); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.vp-doc thead th { + background-color: var(--vp-c-bg-soft); + font-weight: 600; + text-align: left; + padding: var(--space-md); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--vp-c-text-2); +} + +.vp-doc tbody td { + padding: var(--space-md); + font-size: 0.875rem; + border-top: 1px solid var(--vp-c-divider); +} + +.vp-doc tbody tr:hover { + background-color: var(--vp-c-bg-soft); +} + +/* ======================================== + Custom Blocks + ======================================== */ + +.custom-block { + border-radius: var(--radius-lg); + padding: var(--space-lg); + margin: var(--space-lg) 0; + border: 1px solid; + position: relative; +} + +.custom-block.tip { + background-color: var(--vp-c-brand-soft); + border-color: var(--vp-c-brand-1); +} + +.custom-block.info { + background-color: rgba(66, 184, 221, 0.1); + border-color: #42b8dd; +} + +.custom-block.warning { + background-color: rgba(255, 197, 23, 0.1); + border-color: #ffc517; +} + +.custom-block.danger { + background-color: rgba(244, 63, 94, 0.1); + border-color: #f43f5e; +} + +.custom-block-title { + font-weight: 600; + margin-bottom: var(--space-sm); +} + +/* ======================================== + Home Page + ======================================== */ + +.VPHome { + padding-bottom: var(--space-3xl); +} + +.VPHero .name { + font-size: 3.5rem; + font-weight: 900; + letter-spacing: -0.03em; +} + +.VPHero .text { + font-size: 1.5rem; + font-weight: 500; + color: var(--vp-c-text-2); + margin-top: var(--space-sm); +} + +.VPHero .tagline { + font-size: 1.25rem; + color: var(--vp-c-text-3); + margin-top: var(--space-md); + line-height: 1.5; +} + +/* Feature Cards */ +.VPFeatures { + margin-top: var(--space-3xl); +} + +.VPFeature { + border: 1px solid var(--vp-c-divider); + border-radius: var(--radius-xl); + padding: var(--space-xl); + background: var(--vp-c-bg-soft); + height: 100%; +} + +.VPFeature:hover { + border-color: var(--vp-c-brand-1); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.VPFeature .icon { + font-size: 2.5rem; + margin-bottom: var(--space-md); +} + +.VPFeature .title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: var(--space-sm); +} + +.VPFeature .details { + font-size: 0.875rem; + color: var(--vp-c-text-2); + line-height: 1.6; +} + +/* ======================================== + Navigation + ======================================== */ + +.VPNav { + background-color: var(--vp-c-bg); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--vp-c-divider); +} + +.VPNavBar .title { + font-weight: 700; + font-size: 1.125rem; +} + +.VPNavBarMenuLink { + font-weight: 500; +} + +.VPNavBarMenuLink.active { + color: var(--vp-c-brand-1); +} + +/* ======================================== + Sidebar + ======================================== */ + +.VPSidebar { + padding: var(--space-xl) 0; +} + +.VPSidebarItem { + margin-bottom: var(--space-xs); +} + +.VPSidebarItem .text { + font-size: 0.875rem; + font-weight: 500; +} + +.VPSidebarItem.level-0 > .items { + margin-top: var(--space-sm); + margin-bottom: var(--space-lg); +} + +.VPSidebarItem.is-active > .item > .link > .text { + color: var(--vp-c-brand-1); + font-weight: 600; +} + +/* ======================================== + Buttons + ======================================== */ + +.VPButton { + font-weight: 500; + border-radius: var(--radius-md); + padding: 0 var(--space-lg); + height: 40px; + font-size: 0.875rem; + transition: all 200ms ease; + border: 1px solid transparent; +} + +.VPButton.brand { + background-color: var( + --vp-c-brand-3 + ); /* Use darker cyan for better contrast */ + color: white; +} + +.VPButton.brand:hover { + background-color: var(--blac-cyan-darkest); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.VPButton.alt { + background-color: transparent; + color: var(--vp-c-text-1); + border-color: var(--vp-c-divider); +} + +.VPButton.alt:hover { + border-color: var(--vp-c-brand-1); + color: var(--vp-c-brand-1); +} + +/* ======================================== + Search + ======================================== */ + +.DocSearch-Button { + border-radius: var(--radius-md); + padding: 0 var(--space-md); + gap: var(--space-sm); +} + +.DocSearch-Button:hover { + background-color: var(--vp-c-bg-soft); +} + +/* ======================================== + Footer + ======================================== */ + +.VPFooter { + border-top: 1px solid var(--vp-c-divider); + padding: var(--space-2xl) 0; +} + +/* ======================================== + Utilities + ======================================== */ + +/* API Reference sections */ +.api-section { + background-color: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: var(--radius-lg); + padding: var(--space-xl); + margin: var(--space-xl) 0; +} + +.api-section h3 { + margin-top: 0; + padding-bottom: var(--space-md); + border-bottom: 1px solid var(--vp-c-divider); +} + +/* Parameter lists */ +.param-list { + list-style: none; + padding: 0; + margin: var(--space-md) 0; +} + +.param-list li { + padding: var(--space-sm) 0; + border-bottom: 1px solid var(--vp-c-divider); +} + +.param-list li:last-child { + border-bottom: none; +} + +.param-name { + font-family: var(--vp-font-family-mono); + font-weight: 600; + color: var(--vp-c-brand-1); +} + +.param-type { + font-family: var(--vp-font-family-mono); + font-size: 0.875em; + color: var(--vp-c-text-2); +} + +/* ======================================== + Custom inline buttons for better accessibility + ======================================== */ + +/* Override inline button styles that use brand color */ +a[style*='background: var(--vp-c-brand)'], +a[style*='background-color: var(--vp-c-brand)'] { + background-color: var(--vp-c-brand-3) !important; /* Use darker cyan */ +} + +a[style*='background: var(--vp-c-brand)']:hover, +a[style*='background-color: var(--vp-c-brand)']:hover { + background-color: var(--blac-cyan-darkest) !important; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} diff --git a/apps/docs/DOCS_OVERHAUL_PLAN.md b/apps/docs/DOCS_OVERHAUL_PLAN.md new file mode 100644 index 00000000..7c017803 --- /dev/null +++ b/apps/docs/DOCS_OVERHAUL_PLAN.md @@ -0,0 +1,218 @@ +# BlaC Documentation Overhaul Plan + +## Overview + +Complete restructuring and rewrite of the BlaC documentation to provide: + +- Clear philosophy and benefits explanation +- Intuitive learning path for newcomers +- Comprehensive API reference +- Practical examples and patterns +- Minimal, clean design focused on content + +## New Documentation Structure + +### 1. Landing Page (index.md) + +- Hero section with tagline and logo +- Clear value proposition +- Quick example showing the simplicity +- Feature highlights (minimal, focused) +- Clear CTAs to Getting Started and GitHub + +### 2. Introduction & Philosophy + +#### `/introduction.md` + +- What is BlaC and why it exists +- Philosophy: Business Logic as Components +- Comparison with other state management solutions +- When to use BlaC vs alternatives +- Core principles and design decisions + +### 3. Getting Started + +#### `/getting-started/installation.md` + +- Installation instructions +- Basic setup +- TypeScript configuration + +#### `/getting-started/first-cubit.md` + +- Step-by-step first Cubit creation +- Understanding state and methods +- Using with React components +- Common gotchas (arrow functions) + +#### `/getting-started/first-bloc.md` + +- When to use Bloc vs Cubit +- Event-driven architecture +- Creating event classes +- Handling events + +### 4. Core Concepts + +#### `/concepts/state-management.md` + +- State as immutable data +- Unidirectional data flow +- Reactive updates + +#### `/concepts/cubits.md` + +- Simple state containers +- emit() and patch() methods +- Best practices + +#### `/concepts/blocs.md` + +- Event-driven state management +- Event classes and handlers +- Event queue and processing + +#### `/concepts/instance-management.md` + +- Automatic creation and disposal +- Shared vs isolated instances +- keepAlive behavior +- Memory management + +### 5. React Integration + +#### `/react/hooks.md` + +- useBloc hook in detail +- useValue for simple subscriptions +- Dependency tracking and optimization + +#### `/react/patterns.md` + +- Component organization +- State sharing strategies +- Performance optimization + +### 6. API Reference + +#### `/api/core/blac.md` + +- Blac class API +- Configuration options +- Plugin system + +#### `/api/core/cubit.md` + +- Cubit class complete reference +- Methods and properties +- Examples + +#### `/api/core/bloc.md` + +- Bloc class complete reference +- Event handling +- Examples + +#### `/api/react/hooks.md` + +- useBloc +- useValue +- Hook options and behavior + +### 7. Patterns & Recipes + +#### `/patterns/async-operations.md` + +- Loading states +- Error handling +- Cancellation + +#### `/patterns/testing.md` + +- Unit testing Cubits/Blocs +- Testing React components +- Mocking strategies + +#### `/patterns/persistence.md` + +- Using the Persist addon +- Custom persistence strategies + +#### `/patterns/debugging.md` + +- Logging and debugging +- DevTools integration +- Common issues + +### 8. Examples + +#### `/examples/` + +- Counter (simple) +- Todo List (CRUD) +- Authentication Flow +- Real-time Updates +- Form Management + +## Design Principles + +### Content First + +- Clean, minimal design +- Focus on readability +- Clear typography +- Thoughtful spacing + +### Navigation + +- Clear hierarchy +- Progressive disclosure +- Search functionality +- Mobile-friendly + +### Code Examples + +- Syntax highlighting +- Copy buttons +- Runnable examples where possible +- TypeScript-first + +### Visual Design + +- Minimal color palette +- Consistent spacing +- Clear visual hierarchy +- Dark mode support + +## Implementation Steps + +1. Update VitePress configuration +2. Create new page templates +3. Write core content sections +4. Add interactive examples +5. Polish design and navigation +6. Add search functionality +7. Optimize for performance + +## Content Guidelines + +### Writing Style + +- Clear and concise +- Beginner-friendly explanations +- Advanced details in expandable sections +- Consistent terminology + +### Code Style + +- TypeScript for all examples +- Arrow functions for methods +- Meaningful variable names +- Comments only where necessary + +### Examples + +- Start simple, build complexity +- Real-world use cases +- Common patterns +- Anti-patterns to avoid diff --git a/apps/docs/agent_instructions.md b/apps/docs/agent_instructions.md new file mode 100644 index 00000000..9d8e51d2 --- /dev/null +++ b/apps/docs/agent_instructions.md @@ -0,0 +1,734 @@ +# Agent Instructions for BlaC State Management + +This guide helps coding agents correctly implement BlaC state management on the first try. + +## 🚨 Critical Rules - MUST READ + +### 1. ALWAYS Use Arrow Functions for Methods + +```typescript +// ✅ CORRECT - Arrow functions maintain proper this binding +class CounterCubit extends Cubit { + increment = () => { + this.emit({ count: this.state.count + 1 }); + }; +} + +// ❌ WRONG - Regular methods lose this binding when called from React +class CounterCubit extends Cubit { + increment() { + this.emit({ count: this.state.count + 1 }); + } +} +``` + +### 2. State Must Be Immutable + +```typescript +// ❌ WRONG - Mutating state +this.state.count++; +this.emit(this.state); + +// ✅ CORRECT - Creating new state +this.emit({ count: this.state.count + 1 }); + +// ✅ CORRECT - Using spread for objects +this.emit({ ...this.state, count: this.state.count + 1 }); + +// ✅ CORRECT - Using patch for partial updates +this.patch({ count: this.state.count + 1 }); +``` + +### 3. Event Classes for Bloc Pattern + +```typescript +// Define event classes (not plain objects or strings!) +class IncrementEvent { + constructor(public readonly amount: number = 1) {} +} + +class CounterBloc extends Bloc { + constructor() { + super({ count: 0 }); + + // Register handlers in constructor + this.on(IncrementEvent, (event, emit) => { + emit({ count: this.state.count + event.amount }); + }); + } + + // Helper method using arrow function + increment = (amount?: number) => { + this.add(new IncrementEvent(amount)); + }; +} +``` + +## Core Patterns + +### Cubit Pattern (Simple State) + +Use Cubit for straightforward state management: + +```typescript +interface UserState { + user: User | null; + loading: boolean; + error: string | null; +} + +class UserCubit extends Cubit { + constructor() { + super({ user: null, loading: false, error: null }); + } + + loadUser = async (userId: string) => { + this.emit({ ...this.state, loading: true, error: null }); + + try { + const user = await api.fetchUser(userId); + this.emit({ user, loading: false, error: null }); + } catch (error) { + this.emit({ + ...this.state, + loading: false, + error: error.message, + }); + } + }; + + updateName = (name: string) => { + if (!this.state.user) return; + + // Using patch for partial updates + this.patch({ + user: { ...this.state.user, name }, + }); + }; + + logout = () => { + this.emit({ user: null, loading: false, error: null }); + }; +} +``` + +### Bloc Pattern (Event-Driven) + +Use Bloc for complex event-driven state: + +```typescript +// Event classes +class LoadTodos {} +class AddTodo { + constructor(public readonly text: string) {} +} +class ToggleTodo { + constructor(public readonly id: string) {} +} +class DeleteTodo { + constructor(public readonly id: string) {} +} + +type TodoEvent = LoadTodos | AddTodo | ToggleTodo | DeleteTodo; + +interface TodoState { + todos: Todo[]; + loading: boolean; + error: string | null; +} + +class TodoBloc extends Bloc { + constructor() { + super({ todos: [], loading: false, error: null }); + + this.on(LoadTodos, this.onLoadTodos); + this.on(AddTodo, this.onAddTodo); + this.on(ToggleTodo, this.onToggleTodo); + this.on(DeleteTodo, this.onDeleteTodo); + } + + private onLoadTodos = async (_: LoadTodos, emit: Emitter) => { + emit({ ...this.state, loading: true }); + + try { + const todos = await api.fetchTodos(); + emit({ todos, loading: false, error: null }); + } catch (error) { + emit({ ...this.state, loading: false, error: error.message }); + } + }; + + private onAddTodo = (event: AddTodo, emit: Emitter) => { + const newTodo: Todo = { + id: Date.now().toString(), + text: event.text, + completed: false, + }; + + emit({ + ...this.state, + todos: [...this.state.todos, newTodo], + }); + }; + + private onToggleTodo = (event: ToggleTodo, emit: Emitter) => { + emit({ + ...this.state, + todos: this.state.todos.map((todo) => + todo.id === event.id ? { ...todo, completed: !todo.completed } : todo, + ), + }); + }; + + private onDeleteTodo = (event: DeleteTodo, emit: Emitter) => { + emit({ + ...this.state, + todos: this.state.todos.filter((todo) => todo.id !== event.id), + }); + }; + + // Public API methods + loadTodos = () => this.add(new LoadTodos()); + addTodo = (text: string) => this.add(new AddTodo(text)); + toggleTodo = (id: string) => this.add(new ToggleTodo(id)); + deleteTodo = (id: string) => this.add(new DeleteTodo(id)); +} +``` + +## React Integration + +### Basic Hook Usage + +```tsx +import { useBloc } from '@blac/react'; + +function TodoList() { + // Returns tuple: [state, blocInstance] + const [state, bloc] = useBloc(TodoBloc); + + useEffect(() => { + bloc.loadTodos(); + }, [bloc]); + + if (state.loading) return
Loading...
; + if (state.error) return
Error: {state.error}
; + + return ( +
+ {state.todos.map((todo) => ( + bloc.toggleTodo(todo.id)} + onDelete={() => bloc.deleteTodo(todo.id)} + /> + ))} +
+ ); +} +``` + +### Selector Pattern (Optimized Re-renders) + +```tsx +function TodoStats() { + // Only re-render when these specific values change + const [state] = useBloc(TodoBloc, { + selector: (state) => ({ + total: state.todos.length, + completed: state.todos.filter((t) => t.completed).length, + active: state.todos.filter((t) => !t.completed).length, + }), + }); + + return ( +
+

Total: {state.total}

+

Active: {state.active}

+

Completed: {state.completed}

+
+ ); +} +``` + +### Computed Values + +```typescript +class ShoppingCartCubit extends Cubit { + // Computed properties use getters + get totalPrice() { + return this.state.items.reduce( + (sum, item) => sum + (item.price * item.quantity), + 0 + ); + } + + get totalItems() { + return this.state.items.reduce( + (sum, item) => sum + item.quantity, + 0 + ); + } + + get isEmpty() { + return this.state.items.length === 0; + } +} + +// In React +function CartSummary() { + const [state, cart] = useBloc(ShoppingCartCubit); + + return ( +
+

Items: {cart.totalItems}

+

Total: ${cart.totalPrice.toFixed(2)}

+ {cart.isEmpty &&

Your cart is empty

} +
+ ); +} +``` + +## Instance Management + +### Shared Instance (Default) + +```typescript +// All components share the same instance +class AppStateCubit extends Cubit { + // Default behavior - shared across all consumers +} +``` + +### Isolated Instance + +```typescript +// Each component gets its own instance +class FormCubit extends Cubit { + static isolated = true; + + constructor() { + super({ name: '', email: '', message: '' }); + } +} + +// Each form has independent state +function ContactForm() { + const [state, form] = useBloc(FormCubit); + // This instance is unique to this component +} +``` + +### Keep Alive Instance + +```typescript +// Instance persists even when no components use it +class AuthCubit extends Cubit { + static keepAlive = true; + + constructor() { + super({ user: null, token: null }); + } +} +``` + +### Named Instances + +```tsx +// Multiple shared instances with different IDs +function GameScore() { + const [teamA] = useBloc(ScoreCubit, { instanceId: 'team-a' }); + const [teamB] = useBloc(ScoreCubit, { instanceId: 'team-b' }); + + return ( +
+ + +
+ ); +} +``` + +### Props-Based Instances + +```typescript +interface ChatRoomProps { + roomId: string; +} + +class ChatRoomCubit extends Cubit { + constructor(props: ChatRoomProps) { + super({ messages: [], typing: [] }); + this._name = `ChatRoom_${props.roomId}`; + this.connectToRoom(props.roomId); + } + + private connectToRoom = (roomId: string) => { + // Connect to specific room + }; +} + +// Usage +function ChatRoom({ roomId }: { roomId: string }) { + const [state, chat] = useBloc(ChatRoomCubit, { + props: { roomId } + }); + + return
{/* Chat UI */}
; +} +``` + +## Advanced Patterns + +### Services Pattern + +```typescript +// Shared services accessed by multiple blocs +class ApiService { + async fetchUser(id: string): Promise { + // API implementation + } +} + +class AuthService { + private token: string | null = null; + + setToken(token: string) { + this.token = token; + } + + getHeaders() { + return this.token ? { Authorization: `Bearer ${this.token}` } : {}; + } +} + +// Inject services into blocs +class UserProfileCubit extends Cubit { + constructor( + private api: ApiService, + private auth: AuthService, + ) { + super({ user: null, posts: [], loading: false }); + } + + loadProfile = async (userId: string) => { + this.emit({ ...this.state, loading: true }); + + try { + const headers = this.auth.getHeaders(); + const user = await this.api.fetchUser(userId, headers); + this.emit({ ...this.state, user, loading: false }); + } catch (error) { + // Handle error + } + }; +} +``` + +### Plugin System + +```typescript +import { BlacPlugin, BlacLifecycleEvent } from '@blac/core'; + +// Create custom plugin +class LoggerPlugin implements BlacPlugin { + name = 'LoggerPlugin'; + + onEvent(event: BlacLifecycleEvent, bloc: BlocBase, params?: any) { + switch (event) { + case BlacLifecycleEvent.STATE_CHANGED: + console.log(`[${bloc._name}] State:`, bloc.state); + break; + case BlacLifecycleEvent.EVENT_DISPATCHED: + console.log(`[${bloc._name}] Event:`, params); + break; + } + } +} + +// Register globally +Blac.addPlugin(new LoggerPlugin()); + +// Or per-instance +class DebugCubit extends Cubit { + constructor() { + super({}); + this.addPlugin(new LoggerPlugin()); + } +} +``` + +### Persistence Plugin + +```typescript +import { PersistencePlugin } from '@blac/plugin-persistence'; + +class SettingsCubit extends Cubit { + constructor() { + super({ + theme: 'light', + language: 'en', + notifications: true, + }); + + // Add persistence + this.addPlugin( + new PersistencePlugin({ + key: 'app-settings', + storage: localStorage, // or sessionStorage + serialize: JSON.stringify, + deserialize: JSON.parse, + }), + ); + } + + setTheme = (theme: 'light' | 'dark') => { + this.patch({ theme }); + }; +} +``` + +## Testing + +### Basic Testing + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { BlocTest } from '@blac/core/testing'; + +describe('CounterCubit', () => { + beforeEach(() => { + BlocTest.setUp(); + }); + + afterEach(() => { + BlocTest.tearDown(); + }); + + it('should increment counter', () => { + const cubit = BlocTest.create(CounterCubit); + + expect(cubit.state.count).toBe(0); + + cubit.increment(); + expect(cubit.state.count).toBe(1); + + cubit.increment(); + expect(cubit.state.count).toBe(2); + }); +}); +``` + +### Testing Async Operations + +```typescript +import { waitFor } from '@blac/core/testing'; + +it('should load user data', async () => { + const mockUser = { id: '1', name: 'John' }; + api.fetchUser = vi.fn().mockResolvedValue(mockUser); + + const cubit = BlocTest.create(UserCubit); + + cubit.loadUser('1'); + + // Wait for loading state + expect(cubit.state.loading).toBe(true); + + // Wait for loaded state + await waitFor(() => { + expect(cubit.state.loading).toBe(false); + expect(cubit.state.user).toEqual(mockUser); + }); +}); +``` + +### Testing Bloc Events + +```typescript +it('should handle todo events', () => { + const bloc = BlocTest.create(TodoBloc); + + // Add todo + bloc.add(new AddTodo('Test todo')); + expect(bloc.state.todos).toHaveLength(1); + expect(bloc.state.todos[0].text).toBe('Test todo'); + + // Toggle todo + const todoId = bloc.state.todos[0].id; + bloc.add(new ToggleTodo(todoId)); + expect(bloc.state.todos[0].completed).toBe(true); + + // Delete todo + bloc.add(new DeleteTodo(todoId)); + expect(bloc.state.todos).toHaveLength(0); +}); +``` + +## Common Mistakes to Avoid + +### 1. Using Regular Methods Instead of Arrow Functions + +```typescript +// ❌ WRONG +class CounterCubit extends Cubit { + increment() { + this.emit({ count: this.state.count + 1 }); + } +} + +// ✅ CORRECT +class CounterCubit extends Cubit { + increment = () => { + this.emit({ count: this.state.count + 1 }); + }; +} +``` + +### 2. Mutating State + +```typescript +// ❌ WRONG +this.state.items.push(newItem); +this.emit(this.state); + +// ✅ CORRECT +this.emit({ + ...this.state, + items: [...this.state.items, newItem], +}); +``` + +### 3. Not Registering Event Handlers + +```typescript +// ❌ WRONG +class TodoBloc extends Bloc { + handleAddTodo = (event: AddTodo, emit: Emitter) => { + // This won't work - handler not registered! + }; +} + +// ✅ CORRECT +class TodoBloc extends Bloc { + constructor() { + super(initialState); + this.on(AddTodo, this.handleAddTodo); + } + + private handleAddTodo = (event: AddTodo, emit: Emitter) => { + // Now it works! + }; +} +``` + +### 4. Using Strings as Events + +```typescript +// ❌ WRONG +bloc.add('increment'); // Events must be class instances + +// ✅ CORRECT +bloc.add(new IncrementEvent()); +``` + +### 5. Not Using useBloc Hook + +```typescript +// ❌ WRONG - No reactivity +const cubit = new CounterCubit(); +return
{cubit.state.count}
; + +// ✅ CORRECT - Reactive updates +const [state, cubit] = useBloc(CounterCubit); +return
{state.count}
; +``` + +## Quick Reference + +### Creating a Cubit + +```typescript +class NameCubit extends Cubit { + constructor() { + super(initialState); + } + + methodName = () => { + this.emit(newState); + // or + this.patch(partialState); + }; +} +``` + +### Creating a Bloc + +```typescript +class NameBloc extends Bloc { + constructor() { + super(initialState); + this.on(EventClass, this.handleEvent); + } + + private handleEvent = (event: EventClass, emit: Emitter) => { + emit(newState); + }; +} +``` + +### Using in React + +```tsx +// Basic usage +const [state, bloc] = useBloc(BlocOrCubitClass); + +// With options +const [state, bloc] = useBloc(BlocOrCubitClass, { + instanceId: 'unique-id', + props: { + /* props */ + }, + selector: (state) => state.someValue, +}); +``` + +### Configuration Options + +```typescript +// Instance options +static isolated = true; // Each consumer gets own instance +static keepAlive = true; // Persist when no consumers + +// Global config +Blac.setConfig({ + proxyDependencyTracking: true, // Auto dependency tracking + enableLogging: false // Development logging +}); +``` + +## Remember + +1. **Arrow functions** for ALL methods (this is critical!) +2. **Event classes** for Bloc pattern (not strings or plain objects) +3. **Immutable state** (always create new objects) +4. **useBloc hook** for React integration +5. **Register handlers** in constructor for Blocs +6. **Test business logic** separately from UI +7. **Use TypeScript** for full type safety + +## When to Use What + +- **Cubit**: Simple state, direct updates, synchronous logic +- **Bloc**: Complex flows, event sourcing, async operations +- **Isolated**: Form state, modals, component-specific state +- **KeepAlive**: Auth state, app settings, global cache +- **Named instances**: Multiple instances of same logic (e.g., multiple counters) +- **Props-based**: State tied to specific data (e.g., user profile, chat room) + +## Get Help + +- Check `/apps/docs/examples/` for complete examples +- Run tests with `pnpm test` to verify behavior +- Use TypeScript for autocomplete and type checking +- Enable logging during development for debugging diff --git a/apps/docs/api/configuration.md b/apps/docs/api/configuration.md new file mode 100644 index 00000000..31609ea4 --- /dev/null +++ b/apps/docs/api/configuration.md @@ -0,0 +1,222 @@ +# Configuration + +BlaC provides global configuration options to customize its behavior across your entire application. This page covers all available configuration options and how to use them effectively. + +## Overview + +BlaC uses a static configuration system that allows you to modify global behaviors. Configuration changes affect all new instances and behaviors but do not retroactively change existing instances. + +## Setting Configuration + +Use the `Blac.setConfig()` method to update configuration: + +```typescript +import { Blac } from '@blac/core'; + +// Set configuration +Blac.setConfig({ + proxyDependencyTracking: false, +}); +``` + +## Reading Configuration + +Access the current configuration using the `Blac.config` getter: + +```typescript +const currentConfig = Blac.config; +console.log(currentConfig.proxyDependencyTracking); // true (default) +``` + +Note: `Blac.config` returns a readonly copy of the configuration to prevent accidental mutations. + +## Configuration Options + +### `proxyDependencyTracking` + +**Type:** `boolean` +**Default:** `true` + +Controls whether BlaC uses automatic proxy-based dependency tracking for optimized re-renders in React components. + +#### When enabled (default): + +- Components only re-render when properties they actually access change +- BlaC automatically tracks which state properties your components use +- Provides fine-grained reactivity and optimal performance + +```typescript +// With proxy tracking enabled (default) +const MyComponent = () => { + const [state, bloc] = useBloc(UserBloc); + + // Only re-renders when state.name changes + return
{state.name}
; +}; +``` + +#### When disabled: + +- Components re-render on any state change +- Simpler mental model but potentially more re-renders +- Useful for debugging or when proxy behavior causes issues + +```typescript +// Disable proxy tracking globally +Blac.setConfig({ proxyDependencyTracking: false }); + +const MyComponent = () => { + const [state, bloc] = useBloc(UserBloc); + + // Re-renders on ANY state change + return
{state.name}
; +}; +``` + +#### Manual Dependencies Override + +You can always override the global setting by providing manual dependencies: + +```typescript +// This always uses manual dependencies, regardless of global config +const [state, bloc] = useBloc(UserBloc, { + dependencies: (instance) => [instance.state.name, instance.state.email], +}); +``` + +## Best Practices + +### 1. Configure Early + +Set your configuration as early as possible in your application lifecycle: + +```typescript +// app.tsx or index.tsx +import { Blac } from '@blac/core'; + +// Configure before any components mount +Blac.setConfig({ + proxyDependencyTracking: true +}); + +// Then render your app +createRoot(document.getElementById('root')!).render(); +``` + +### 2. Environment-Based Configuration + +Adjust configuration based on your environment: + +```typescript +Blac.setConfig({ + proxyDependencyTracking: process.env.NODE_ENV === 'production', +}); +``` + +### 3. Testing Configuration + +Reset configuration between tests to ensure isolation: + +```typescript +describe('MyComponent', () => { + const originalConfig = { ...Blac.config }; + + afterEach(() => { + Blac.setConfig(originalConfig); + }); + + it('works without proxy tracking', () => { + Blac.setConfig({ proxyDependencyTracking: false }); + // ... test implementation + }); +}); +``` + +## Performance Considerations + +### Proxy Dependency Tracking + +**Benefits:** + +- Minimal re-renders - components only update when accessed properties change +- Automatic optimization without manual work +- Ideal for complex state objects with many properties + +**Costs:** + +- Small overhead from proxy creation +- May interfere with certain debugging tools +- Can be confusing if you expect all state changes to trigger re-renders + +**When to disable:** + +- Debugging re-render issues +- Working with state objects that don't benefit from fine-grained tracking +- Compatibility issues with certain tools or libraries + +## TypeScript Support + +BlaC exports the `BlacConfig` interface for type-safe configuration: + +```typescript +import { BlacConfig } from '@blac/core'; + +const myConfig: Partial = { + proxyDependencyTracking: false, +}; + +Blac.setConfig(myConfig); +``` + +## Logging Configuration + +BlaC also provides static properties for debugging: + +```typescript +// Enable logging +Blac.enableLog = true; +Blac.logLevel = 'log'; // or 'warn' + +// Custom log spy for testing +Blac.logSpy = (args) => { + // Custom logging logic +}; +``` + +## Plugin Configuration + +BlaC's plugin system can be configured globally: + +```typescript +import { Blac } from '@blac/core'; +import { LoggerPlugin } from './plugins'; + +// Add global plugins +Blac.plugins.add(new LoggerPlugin()); + +// Configure plugins based on environment +if (process.env.NODE_ENV === 'development') { + Blac.plugins.add(new DevToolsPlugin()); +} +``` + +See the [Plugin System documentation](/plugins/overview) for more details. + +## Migration Guide + +If you're upgrading from a version without configuration support: + +1. **Default behavior is unchanged** - Proxy tracking is enabled by default +2. **No code changes required** - Existing code continues to work +3. **Opt-in to changes** - Explicitly disable features if needed + +```typescript +// Old behavior (proxy tracking always on) +const [state, bloc] = useBloc(MyBloc); + +// Still works exactly the same with default config +const [state, bloc] = useBloc(MyBloc); + +// New option to disable if needed +Blac.setConfig({ proxyDependencyTracking: false }); +``` diff --git a/apps/docs/api/core-classes.md b/apps/docs/api/core-classes.md index e748f1cd..e7262c09 100644 --- a/apps/docs/api/core-classes.md +++ b/apps/docs/api/core-classes.md @@ -2,44 +2,45 @@ Blac provides three primary classes for state management: -## BlocBase +## BlocBase `BlocBase` is the abstract base class for all state containers in Blac. ### Type Parameters - `S` - The state type -- `P` - The props type (optional) ### Properties -| Name | Type | Description | -|------|------|-------------| -| `state` | `S` | The current state of the container | -| `props` | `P \| null` | Props passed during Bloc instance creation (can be null) | -| `lastUpdate` | `number` | Timestamp when the state was last updated | +| Name | Type | Description | +| ------------------- | -------- | ----------------------------------------- | +| `state` | `S` | The current state of the container | +| `lastUpdate` | `number` | Timestamp when the state was last updated | +| `subscriptionCount` | `number` | Number of active subscriptions | ### Static Properties -| Name | Type | Default | Description | -|------|------|---------|-------------| -| `isolated` | `boolean` | `false` | When true, every consumer will receive its own unique instance | -| `keepAlive` | `boolean` | `false` | When true, the instance persists even when no components are using it | +| Name | Type | Default | Description | +| ----------- | -------------- | ----------- | --------------------------------------------------------------------- | +| `isolated` | `boolean` | `false` | When true, every consumer will receive its own unique instance | +| `keepAlive` | `boolean` | `false` | When true, the instance persists even when no components are using it | +| `plugins` | `BlocPlugin[]` | `undefined` | Array of plugins to automatically attach to instances | ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `on` | `event: BlacEvent, listener: StateListener, signal?: AbortSignal` | `() => void` | Subscribes to state changes and returns an unsubscribe function | +| Name | Parameters | Return Type | Description | +| ----------------------- | ----------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- | +| `subscribe` | `callback: (state: S) => void` | `() => void` | Subscribes to state changes and returns an unsubscribe function | +| `subscribeWithSelector` | `selector: (state: S) => T, callback: (value: T) => void, equalityFn?: (a: T, b: T) => boolean` | `() => void` | Subscribe with a selector for optimized updates | +| `onDispose` | Optional method | `void` | Override to perform cleanup when the instance is disposed | -## Cubit +## Cubit `Cubit` is a simple state container that extends `BlocBase`. It's ideal for simpler state management needs. ### Type Parameters - `S` - The state type -- `P` - The props type (optional, defaults to null) ### Constructor @@ -49,10 +50,10 @@ constructor(initialState: S) ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `emit` | `state: S` | `void` | Replaces the entire state | -| `patch` | `statePatch: S extends object ? Partial : S, ignoreChangeCheck?: boolean` | `void` | Updates specific properties of the state | +| Name | Parameters | Return Type | Description | +| ------- | ---------------------------------------------------------------------------- | ----------- | ---------------------------------------- | +| `emit` | `state: S` | `void` | Replaces the entire state | +| `patch` | `statePatch: S extends object ? Partial : S, ignoreChangeCheck?: boolean` | `void` | Updates specific properties of the state | ### Example @@ -62,6 +63,7 @@ class CounterCubit extends Cubit<{ count: number }> { super({ count: 0 }); } + // IMPORTANT: Always use arrow functions for React compatibility increment = () => { this.emit({ count: this.state.count + 1 }); }; @@ -77,15 +79,14 @@ class CounterCubit extends Cubit<{ count: number }> { } ``` -## Bloc +## Bloc -`Bloc` is a more sophisticated state container that follows the reducer pattern. It extends `BlocBase` and adds action handling. +`Bloc` is a more sophisticated state container that uses an event-handler pattern. It extends `BlocBase` and manages state transitions by registering handlers for specific event classes. ### Type Parameters - `S` - The state type -- `A` - The action type -- `P` - The props type (optional) +- `A` - The base type or union of event classes that this Bloc can process (must be class instances, not plain objects) ### Constructor @@ -95,57 +96,64 @@ constructor(initialState: S) ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `add` | `action: A` | `void` | Dispatches an action to the reducer | -| `reducer` | `action: A, state: S` | `S` | Determines how state changes in response to actions (must be implemented) | +| Name | Parameters | Return Type | Description | +| ----- | ------------------------------------------------------------------------------------------------------------------------ | --------------- | ---------------------------------------------------------------------------------------------- | +| `on` | `eventConstructor: new (...args: any[]) => E, handler: (event: E, emit: (newState: S) => void) => void \| Promise` | `void` | Registers an event handler for a specific event class. Protected method called in constructor. | +| `add` | `event: A` | `Promise` | Dispatches an event instance to its registered handler. Events are processed sequentially. | ### Example ```tsx -// Define actions -type CounterAction = - | { type: 'increment' } - | { type: 'decrement' } - | { type: 'reset' }; +// Define event classes +class IncrementEvent { + constructor(public readonly value: number = 1) {} +} +class DecrementEvent {} +class ResetEvent {} + +// Union type for all possible event classes (optional but can be useful) +type CounterEvent = IncrementEvent | DecrementEvent | ResetEvent; -class CounterBloc extends Bloc<{ count: number }, CounterAction> { +class CounterBloc extends Bloc<{ count: number }, CounterEvent> { constructor() { super({ count: 0 }); + + // Register event handlers + this.on(IncrementEvent, this.handleIncrement); + this.on(DecrementEvent, this.handleDecrement); + this.on(ResetEvent, this.handleReset); } - // Implement the reducer method - reducer = (action: CounterAction, state: { count: number }) => { - switch (action.type) { - case 'increment': - return { count: state.count + 1 }; - case 'decrement': - return { count: state.count - 1 }; - case 'reset': - return { count: 0 }; - default: - return state; - } + // IMPORTANT: Always use arrow functions for proper this binding + private handleIncrement = ( + event: IncrementEvent, + emit: (state: { count: number }) => void, + ) => { + emit({ count: this.state.count + event.value }); }; - // Helper methods to dispatch actions - increment = () => this.add({ type: 'increment' }); - decrement = () => this.add({ type: 'decrement' }); - reset = () => this.add({ type: 'reset' }); -} -``` - -## BlacEvent + private handleDecrement = ( + event: DecrementEvent, + emit: (state: { count: number }) => void, + ) => { + emit({ count: this.state.count - 1 }); + }; -`BlacEvent` is an enum that defines the different events that can be dispatched by Blac. + private handleReset = ( + event: ResetEvent, + emit: (state: { count: number }) => void, + ) => { + emit({ count: 0 }); + }; -| Event | Description | -|-------|-------------| -| `StateChange` | Triggered when a state changes | -| `Error` | Triggered when an error occurs | -| `Action` | Triggered when an action is dispatched | + // Helper methods to dispatch events (optional but common) + increment = (value?: number) => this.add(new IncrementEvent(value)); + decrement = () => this.add(new DecrementEvent()); + reset = () => this.add(new ResetEvent()); +} +``` ## Choosing Between Cubit and Bloc -- Use **Cubit** when you have simple state logic and don't need the reducer pattern -- Use **Bloc** when you have complex state transitions, want to leverage the reducer pattern, or need a more formal action-based approach \ No newline at end of file +- Use **Cubit** for simpler state logic where direct state emission (`emit`, `patch`) is sufficient. +- Use **Bloc** for more complex state logic, when you want to process distinct event types with dedicated handlers, or when you need a more formal event-driven approach to manage state transitions. diff --git a/apps/docs/api/core/blac.md b/apps/docs/api/core/blac.md new file mode 100644 index 00000000..a08218a0 --- /dev/null +++ b/apps/docs/api/core/blac.md @@ -0,0 +1,576 @@ +# Blac Class + +The `Blac` class is the central orchestrator of BlaC's state management system. It manages all Bloc/Cubit instances, handles lifecycle events, and provides configuration options. + +## Import + +```typescript +import { Blac } from '@blac/core'; +``` + +## Static Properties + +### enableLog + +Enable or disable console logging for debugging. + +```typescript +static enableLog: boolean = false; +``` + +Example: + +```typescript +// Enable logging in development +if (process.env.NODE_ENV === 'development') { + Blac.enableLog = true; +} +``` + +### logLevel + +Set the minimum log level for console output. + +```typescript +static logLevel: 'warn' | 'log' = 'warn'; +``` + +Example: + +```typescript +// Show all logs including debug logs +Blac.logLevel = 'log'; + +// Only show warnings and errors (default) +Blac.logLevel = 'warn'; +``` + +### logSpy + +Set a custom function to intercept all log messages (useful for testing). + +```typescript +static logSpy: ((...args: unknown[]) => void) | null = null; +``` + +Example: + +```typescript +// Capture logs in tests +const logs: any[] = []; +Blac.logSpy = (...args) => logs.push(args); +``` + +## Static Methods + +### setConfig() + +Configure global BlaC behavior. + +```typescript +static setConfig(config: Partial): void +``` + +#### BlacConfig Interface + +```typescript +interface BlacConfig { + proxyDependencyTracking?: boolean; +} +``` + +#### Configuration Options + +| Option | Type | Default | Description | +| ------------------------- | --------- | ------- | ------------------------------------ | +| `proxyDependencyTracking` | `boolean` | `true` | Enable automatic render optimization | + +Example: + +```typescript +Blac.setConfig({ + proxyDependencyTracking: true, +}); +``` + +### log() + +Log a message if logging is enabled. + +```typescript +static log(...args: any[]): void +``` + +Example: + +```typescript +Blac.log('State updated:', newState); +``` + +### warn() + +Log a warning if warnings are enabled. + +```typescript +static warn(...args: any[]): void +``` + +Example: + +```typescript +Blac.warn('Deprecated feature used'); +``` + +### error() + +Log an error message (when logging is enabled at 'log' level). + +```typescript +static error(...args: any[]): void +``` + +Example: + +```typescript +Blac.error('Failed to update state:', error); +``` + +### getBloc() + +Get or create a Bloc/Cubit instance. This is the primary method for getting bloc instances. + +```typescript +static getBloc>( + blocClass: BlocConstructor, + options?: GetBlocOptions +): B +``` + +#### GetBlocOptions Interface + +```typescript +interface GetBlocOptions> { + id?: string; + selector?: BlocHookDependencyArrayFn>; + constructorParams?: ConstructorParameters>[]; + onMount?: (bloc: B) => void; + instanceRef?: string; + throwIfNotFound?: boolean; + forceNewInstance?: boolean; +} +``` + +Example: + +```typescript +// Get or create with default ID +const counter = Blac.getBloc(CounterCubit); + +// Get or create with custom ID +const userCounter = Blac.getBloc(CounterCubit, { id: 'user-123' }); + +// Get or create with constructor params +const chat = Blac.getBloc(ChatCubit, { + id: 'room-123', + constructorParams: [{ roomId: '123', userId: 'user-456' }], +}); +``` + +### disposeBloc() + +Manually dispose a specific Bloc/Cubit instance. + +```typescript +static disposeBloc(bloc: BlocBase): void +``` + +Example: + +```typescript +// Get bloc instance first +const counter = Blac.getBloc(CounterCubit); + +// Later dispose it +Blac.disposeBloc(counter); +``` + +### disposeBlocs() + +Dispose all blocs matching a predicate function. + +```typescript +static disposeBlocs(predicate: (bloc: BlocBase) => boolean): void +``` + +Example: + +```typescript +// Dispose all CounterCubit instances +Blac.disposeBlocs((bloc) => bloc instanceof CounterCubit); + +// Dispose all blocs with specific ID pattern +Blac.disposeBlocs((bloc) => bloc._id.toString().startsWith('temp-')); +``` + +### disposeKeepAliveBlocs() + +Dispose all keep-alive blocs, optionally filtered by type. + +```typescript +static disposeKeepAliveBlocs>(blocClass?: B): void +``` + +Example: + +```typescript +// Dispose all keep-alive blocs +Blac.disposeKeepAliveBlocs(); + +// Dispose only keep-alive CounterCubit instances +Blac.disposeKeepAliveBlocs(CounterCubit); +``` + +### resetInstance() + +Reset the global Blac instance, clearing all registrations except keep-alive blocs. + +```typescript +static resetInstance(): void +``` + +Example: + +```typescript +// Reset after tests +afterEach(() => { + Blac.resetInstance(); +}); +``` + +## Plugin System + +### instance.plugins.add() + +Register a global plugin to the system plugin registry. + +```typescript +Blac.instance.plugins.add(plugin: BlacPlugin): void +``` + +Example: + +```typescript +// Create and add a plugin +const loggingPlugin: BlacPlugin = { + name: 'LoggingPlugin', + version: '1.0.0', + onStateChanged: (bloc, previousState, currentState) => { + console.log(`[${bloc._name}] State changed`, { + previousState, + currentState, + }); + }, +}; + +Blac.instance.plugins.add(loggingPlugin); +``` + +#### BlacPlugin Interface + +```typescript +interface BlacPlugin { + readonly name: string; + readonly version: string; + readonly capabilities?: PluginCapabilities; + + // Lifecycle hooks - all synchronous + beforeBootstrap?(): void; + afterBootstrap?(): void; + beforeShutdown?(): void; + afterShutdown?(): void; + + // System-wide observations + onBlocCreated?(bloc: BlocBase): void; + onBlocDisposed?(bloc: BlocBase): void; + onStateChanged?( + bloc: BlocBase, + previousState: any, + currentState: any, + ): void; + onEventAdded?(bloc: Bloc, event: any): void; + onError?(error: Error, bloc: BlocBase, context: ErrorContext): void; + + // Adapter lifecycle hooks + onAdapterCreated?(adapter: any, metadata: AdapterMetadata): void; + onAdapterMount?(adapter: any, metadata: AdapterMetadata): void; + onAdapterUnmount?(adapter: any, metadata: AdapterMetadata): void; + onAdapterRender?(adapter: any, metadata: AdapterMetadata): void; + onAdapterDisposed?(adapter: any, metadata: AdapterMetadata): void; +} +``` + +Example: Logging Plugin + +```typescript +const loggingPlugin: BlacPlugin = { + name: 'LoggingPlugin', + version: '1.0.0', + + onBlocCreated: (bloc) => { + console.log(`[BlaC] Created ${bloc._name}`); + }, + + onStateChanged: (bloc, previousState, currentState) => { + console.log(`[BlaC] ${bloc._name} state changed:`, { + old: previousState, + new: currentState, + }); + }, + + onBlocDisposed: (bloc) => { + console.log(`[BlaC] Disposed ${bloc._name}`); + }, +}; + +Blac.instance.plugins.add(loggingPlugin); +``` + +Example: State Persistence Plugin + +```typescript +const persistencePlugin: BlacPlugin = { + name: 'PersistencePlugin', + version: '1.0.0', + + onBlocCreated: (bloc) => { + // Load persisted state + const key = `blac_${bloc._name}_${bloc._id}`; + const saved = localStorage.getItem(key); + if (saved && 'emit' in bloc) { + (bloc as any).emit(JSON.parse(saved)); + } + }, + + onStateChanged: (bloc, previousState, currentState) => { + // Save state changes + const key = `blac_${bloc._name}_${bloc._id}`; + localStorage.setItem(key, JSON.stringify(currentState)); + }, +}; + +Blac.instance.plugins.add(persistencePlugin); +``` + +Example: Analytics Plugin + +```typescript +const analyticsPlugin: BlacPlugin = { + name: 'AnalyticsPlugin', + version: '1.0.0', + + onBlocCreated: (bloc) => { + analytics.track('bloc_created', { + type: bloc._name, + timestamp: Date.now(), + }); + }, + + onStateChanged: (bloc, previousState, currentState) => { + if (bloc._name === 'CartCubit') { + const cartState = currentState as CartState; + analytics.track('cart_updated', { + itemCount: cartState.items.length, + total: cartState.total, + }); + } + }, + + onEventAdded: (bloc, event) => { + // Track important events + if (event.constructor.name === 'CheckoutStarted') { + analytics.track('checkout_started', { + blocName: bloc._name, + timestamp: Date.now(), + }); + } + }, +}; + +Blac.instance.plugins.add(analyticsPlugin); +``` + +## Instance Management + +### Lifecycle Flow + +```mermaid +graph TD + A[Component requests instance] --> B{Instance exists?} + B -->|No| C[beforeCreate hook] + C --> D[Create instance] + D --> E[afterCreate hook] + B -->|Yes| F[Return existing] + E --> F + F --> G[Component uses instance] + G --> H[Component unmounts] + H --> I{Last consumer?} + I -->|Yes| J{keepAlive?} + J -->|No| K[beforeDispose hook] + K --> L[Dispose instance] + L --> M[afterDispose hook] + J -->|Yes| N[Keep in memory] + I -->|No| O[Decrease ref count] +``` + +### Instance Storage + +Internally, BlaC stores instances in a Map: + +```typescript +// Simplified internal structure +class Blac { + private static instances = new Map(); + + private static getInstanceId( + blocClass: Constructor> | string, + id?: string, + ): string { + if (typeof blocClass === 'string') return blocClass; + return id || blocClass.name; + } +} +``` + +## Debugging + +### Instance Inspection + +```typescript +// Log all active instances +if (Blac.enableLog) { + const instances = (Blac as any).instances; + instances.forEach((instance, id) => { + console.log(`Instance ${id}:`, { + state: instance.bloc.state, + consumers: instance.consumers.size, + props: instance.bloc.props, + }); + }); +} +``` + +## Best Practices + +### 1. Configuration + +Set configuration once at app startup: + +```typescript +// main.ts or index.ts +Blac.setConfig({ + enableLog: process.env.NODE_ENV === 'development', + proxyDependencyTracking: true, +}); +``` + +### 2. Plugin Registration + +Register plugins before creating any instances: + +```typescript +// Register plugins first +Blac.instance.plugins.add(loggingPlugin); +Blac.instance.plugins.add(persistencePlugin); + +// Then render app +ReactDOM.render(, document.getElementById('root')); +``` + +### 3. Testing + +Reset state between tests: + +```typescript +beforeEach(() => { + Blac.setConfig({ proxyDependencyTracking: true }); + Blac.resetInstance(); +}); + +afterEach(() => { + Blac.resetInstance(); +}); +``` + +### 4. Manual Instance Management + +Avoid manual instance management unless necessary: + +```typescript +// ✅ Preferred: Let useBloc handle lifecycle +function Component() { + const [state, cubit] = useBloc(CounterCubit); +} + +// ⚠️ Avoid: Manual management +const counter = Blac.getBloc(CounterCubit); +// Remember to dispose when done +Blac.disposeBloc(counter); +``` + +## Additional Static Methods + +### getAllBlocs() + +Get all instances of a specific bloc class. + +```typescript +static getAllBlocs>( + blocClass: B, + options?: { searchIsolated?: boolean } +): InstanceType[] +``` + +Example: + +```typescript +// Get all CounterCubit instances +const allCounters = Blac.getAllBlocs(CounterCubit); + +// Get only non-isolated instances +const sharedCounters = Blac.getAllBlocs(CounterCubit, { + searchIsolated: false, +}); +``` + +### getMemoryStats() + +Get memory usage statistics for debugging. + +```typescript +static getMemoryStats(): { + totalBlocs: number; + registeredBlocs: number; + isolatedBlocs: number; + keepAliveBlocs: number; +} +``` + +### validateConsumers() + +Validate consumer integrity across all blocs. + +```typescript +static validateConsumers(): { valid: boolean; errors: string[] } +``` + +## Summary + +The Blac class provides: + +- **Global instance management**: Centralized control over all state containers +- **Configuration**: Customize behavior for your app's needs +- **Plugin system**: Extend functionality with custom logic +- **Debugging tools**: Inspect and monitor instances +- **Lifecycle hooks**: React to instance creation and disposal + +It's the foundation that makes BlaC's automatic instance management possible. diff --git a/apps/docs/api/core/bloc.md b/apps/docs/api/core/bloc.md new file mode 100644 index 00000000..ab55af3f --- /dev/null +++ b/apps/docs/api/core/bloc.md @@ -0,0 +1,745 @@ +# Bloc Class API + +The `Bloc` class provides event-driven state management by processing event instances through registered handlers. It extends `BlocBase` for a more structured approach than `Cubit`. + +## Class Definition + +```typescript +abstract class Bloc extends BlocBase +``` + +**Type Parameters:** + +- `S` - The state type +- `A` - The base action/event type with proper constraints (must be class instances) + +## Constructor + +```typescript +constructor(initialState: S) +``` + +**Parameters:** + +- `initialState` - The initial state value + +**Example:** + +```typescript +// Define events +class Increment {} +class Decrement {} +type CounterEvent = Increment | Decrement; + +// Create Bloc +class CounterBloc extends Bloc { + constructor() { + super(0); // Initial state + + // Register handlers - can use inline arrow functions for simple cases + this.on(Increment, (event, emit) => emit(this.state + 1)); + this.on(Decrement, (event, emit) => emit(this.state - 1)); + } + + // For complex handlers, use arrow function class methods: + // private handleIncrement = (event: Increment, emit: (state: number) => void) => { + // emit(this.state + 1); + // }; +} +``` + +## Properties + +### state + +The current state value (inherited from BlocBase). + +```typescript +get state(): S +``` + +### lastUpdate + +Timestamp of the last state update (inherited from BlocBase). + +```typescript +get lastUpdate(): number +``` + +## Methods + +### on (Event Registration) + +Register a handler for a specific event class. + +```typescript +on( + eventConstructor: new (...args: any[]) => T, + handler: (event: T, emit: (newState: S) => void) => void | Promise +): void +``` + +**Parameters:** + +- `eventConstructor` - The event class constructor +- `handler` - Function to handle the event + +**Handler Parameters:** + +- `event` - The event instance +- `emit` - Function to emit new state + +**Example:** + +```typescript +class TodoBloc extends Bloc { + constructor() { + super({ items: [], filter: 'all' }); + + // Sync handler + this.on(AddTodo, (event, emit) => { + const newTodo = { id: Date.now(), text: event.text, done: false }; + emit({ + ...this.state, + items: [...this.state.items, newTodo], + }); + }); + + // Async handler + this.on(LoadTodos, async (event, emit) => { + emit({ ...this.state, isLoading: true }); + + try { + const todos = await api.getTodos(); + emit({ items: todos, isLoading: false, error: null }); + } catch (error) { + emit({ ...this.state, isLoading: false, error: error.message }); + } + }); + } +} +``` + +### add + +Dispatch an event to be processed by its registered handler. Events are processed sequentially - if multiple events are added, they will be queued and processed one at a time. + +```typescript +add(event: A): Promise +``` + +**Parameters:** + +- `event` - The event instance to process + +**Returns:** + +A Promise that resolves when the event has been processed. + +**Example:** + +```typescript +const bloc = new TodoBloc(); + +// Dispatch events +bloc.add(new AddTodo('Learn BlaC')); +bloc.add(new ToggleTodo(todoId)); +bloc.add(new LoadTodos()); + +// Helper methods pattern +class TodoBloc extends Bloc { + // ... constructor ... + + // Helper methods for cleaner API + addTodo = (text: string) => this.add(new AddTodo(text)); + toggleTodo = (id: number) => this.add(new ToggleTodo(id)); + loadTodos = () => this.add(new LoadTodos()); +} +``` + +## Inherited from BlocBase + +### Properties + +#### state + +The current state value. + +```typescript +get state(): S +``` + +#### lastUpdate + +Timestamp of the last state update. + +```typescript +get lastUpdate(): number +``` + +#### subscriptionCount + +Get the current number of active subscriptions. + +```typescript +get subscriptionCount(): number +``` + +### Methods + +#### subscribe() + +Subscribe to state changes. + +```typescript +subscribe(callback: (state: S) => void): () => void +``` + +**Example:** + +```typescript +const bloc = new CounterBloc(); +const unsubscribe = bloc.subscribe((state) => { + console.log('State changed to:', state); +}); + +// Later: cleanup +unsubscribe(); +``` + +#### subscribeWithSelector() + +Subscribe with a selector for optimized updates. + +```typescript +subscribeWithSelector( + selector: (state: S) => T, + callback: (value: T) => void, + equalityFn?: (a: T, b: T) => boolean +): () => void +``` + +**Example:** + +```typescript +// Only notified when todos length changes +const unsubscribe = bloc.subscribeWithSelector( + (state) => state.todos.length, + (count) => console.log('Todo count:', count), +); +``` + +### Static Properties + +#### isolated + +When `true`, each component gets its own instance. + +```typescript +static isolated: boolean = false +``` + +#### keepAlive + +When `true`, instance persists even with no consumers. + +```typescript +static keepAlive: boolean = false +``` + +#### plugins + +Array of plugins to automatically attach to this Bloc class. + +```typescript +static plugins?: BlocPlugin[] +``` + +### onDispose + +Override this method to perform cleanup when the Bloc is disposed. This is called automatically when the last consumer unsubscribes and `keepAlive` is false. + +```typescript +onDispose?: () => void +``` + +**Example:** + +```typescript +class DataBloc extends Bloc { + private subscription?: Subscription; + + constructor() { + super(initialState); + this.setupHandlers(); + this.subscription = dataStream.subscribe((data) => { + this.add(new DataReceived(data)); + }); + } + + onDispose = () => { + this.subscription?.unsubscribe(); + console.log('DataBloc cleaned up'); + }; +} +``` + +## Event Classes + +Events are plain classes that carry data: + +### Simple Events + +```typescript +// No data +class RefreshRequested {} + +// With data +class UserSelected { + constructor(public readonly userId: string) {} +} + +// Multiple parameters +class FilterChanged { + constructor( + public readonly category: string, + public readonly sortBy: 'name' | 'date' | 'price', + ) {} +} +``` + +### Event Inheritance + +```typescript +// Base event for common properties +abstract class TodoEvent { + constructor(public readonly timestamp: Date = new Date()) {} +} + +// Specific events +class AddTodo extends TodoEvent { + constructor(public readonly text: string) { + super(); + } +} + +class RemoveTodo extends TodoEvent { + constructor(public readonly id: string) { + super(); + } +} +``` + +### Complex Events + +```typescript +interface SearchOptions { + includeArchived: boolean; + limit: number; + offset: number; +} + +class SearchRequested { + constructor( + public readonly query: string, + public readonly options: SearchOptions = { + includeArchived: false, + limit: 20, + offset: 0, + }, + ) {} +} +``` + +## Event Processing + +### Sequential Processing + +Events are processed one at a time in order: + +```typescript +class SequentialBloc extends Bloc { + constructor() { + super(initialState); + + this.on(SlowEvent, async (event, emit) => { + console.log('Processing started'); + await sleep(1000); + console.log('Processing finished'); + emit(newState); + }); + } +} + +// Events queued +bloc.add(new SlowEvent()); // Starts immediately +bloc.add(new SlowEvent()); // Waits for first to complete +bloc.add(new SlowEvent()); // Waits for second to complete +``` + +### Error Handling + +Errors in handlers are caught and logged: + +```typescript +this.on(RiskyEvent, async (event, emit) => { + try { + const data = await riskyOperation(); + emit({ ...this.state, data }); + } catch (error) { + // Handle error gracefully + emit({ ...this.state, error: error.message }); + + // Or re-throw to stop processing + throw error; + } +}); +``` + +### Event Transformation + +Transform events before processing: + +```typescript +class DebouncedSearchBloc extends Bloc { + private debounceTimer?: NodeJS.Timeout; + + constructor() { + super({ query: '', results: [] }); + + this.on(QueryChanged, (event, emit) => { + // Clear previous timer + clearTimeout(this.debounceTimer); + + // Update query immediately + emit({ ...this.state, query: event.query }); + + // Debounce the search + this.debounceTimer = setTimeout(() => { + this.add(new ExecuteSearch(event.query)); + }, 300); + }); + + this.on(ExecuteSearch, this.handleSearch); + } +} +``` + +## Complete Examples + +### Authentication Bloc + +```typescript +// Events +class LoginRequested { + constructor( + public readonly email: string, + public readonly password: string, + ) {} +} + +class LogoutRequested {} + +class SessionRestored { + constructor(public readonly user: User) {} +} + +type AuthEvent = LoginRequested | LogoutRequested | SessionRestored; + +// State +interface AuthState { + isAuthenticated: boolean; + user: User | null; + isLoading: boolean; + error: string | null; +} + +// Bloc +class AuthBloc extends Bloc { + constructor() { + super({ + isAuthenticated: false, + user: null, + isLoading: false, + error: null, + }); + + this.on(LoginRequested, this.handleLogin); + this.on(LogoutRequested, this.handleLogout); + this.on(SessionRestored, this.handleSessionRestored); + + // Check for existing session + this.checkSession(); + } + + private handleLogin = async ( + event: LoginRequested, + emit: (state: AuthState) => void, + ) => { + emit({ ...this.state, isLoading: true, error: null }); + + try { + const { user, token } = await api.login(event.email, event.password); + localStorage.setItem('token', token); + + emit({ + isAuthenticated: true, + user, + isLoading: false, + error: null, + }); + } catch (error) { + emit({ + ...this.state, + isLoading: false, + error: error instanceof Error ? error.message : 'Login failed', + }); + } + }; + + private handleLogout = async ( + _: LogoutRequested, + emit: (state: AuthState) => void, + ) => { + localStorage.removeItem('token'); + await api.logout(); + + emit({ + isAuthenticated: false, + user: null, + isLoading: false, + error: null, + }); + }; + + private handleSessionRestored = ( + event: SessionRestored, + emit: (state: AuthState) => void, + ) => { + emit({ + isAuthenticated: true, + user: event.user, + isLoading: false, + error: null, + }); + }; + + private checkSession = async () => { + const token = localStorage.getItem('token'); + if (!token) return; + + try { + const user = await api.getCurrentUser(); + this.add(new SessionRestored(user)); + } catch { + localStorage.removeItem('token'); + } + }; + + // Helper methods + login = (email: string, password: string) => { + this.add(new LoginRequested(email, password)); + }; + + logout = () => this.add(new LogoutRequested()); +} +``` + +### Shopping Cart Bloc + +```typescript +// Events +abstract class CartEvent {} + +class AddItem extends CartEvent { + constructor( + public readonly product: Product, + public readonly quantity: number = 1, + ) { + super(); + } +} + +class RemoveItem extends CartEvent { + constructor(public readonly productId: string) { + super(); + } +} + +class UpdateQuantity extends CartEvent { + constructor( + public readonly productId: string, + public readonly quantity: number, + ) { + super(); + } +} + +class ApplyCoupon extends CartEvent { + constructor(public readonly code: string) { + super(); + } +} + +// State +interface CartState { + items: CartItem[]; + subtotal: number; + discount: number; + total: number; + coupon: Coupon | null; + isApplyingCoupon: boolean; + error: string | null; +} + +// Bloc +class CartBloc extends Bloc { + constructor() { + super({ + items: [], + subtotal: 0, + discount: 0, + total: 0, + coupon: null, + isApplyingCoupon: false, + error: null, + }); + + this.on(AddItem, this.handleAddItem); + this.on(RemoveItem, this.handleRemoveItem); + this.on(UpdateQuantity, this.handleUpdateQuantity); + this.on(ApplyCoupon, this.handleApplyCoupon); + } + + private handleAddItem = ( + event: AddItem, + emit: (state: CartState) => void, + ) => { + const existing = this.state.items.find( + (item) => item.product.id === event.product.id, + ); + + let newItems: CartItem[]; + if (existing) { + newItems = this.state.items.map((item) => + item.product.id === event.product.id + ? { ...item, quantity: item.quantity + event.quantity } + : item, + ); + } else { + newItems = [ + ...this.state.items, + { + product: event.product, + quantity: event.quantity, + }, + ]; + } + + emit(this.calculateTotals({ ...this.state, items: newItems })); + }; + + private handleApplyCoupon = async ( + event: ApplyCoupon, + emit: (state: CartState) => void, + ) => { + emit({ ...this.state, isApplyingCoupon: true, error: null }); + + try { + const coupon = await api.validateCoupon(event.code); + emit( + this.calculateTotals({ + ...this.state, + coupon, + isApplyingCoupon: false, + }), + ); + } catch (error) { + emit({ + ...this.state, + isApplyingCoupon: false, + error: 'Invalid coupon code', + }); + } + }; + + private calculateTotals(state: CartState): CartState { + const subtotal = state.items.reduce( + (sum, item) => sum + item.product.price * item.quantity, + 0, + ); + + let discount = 0; + if (state.coupon) { + discount = + state.coupon.type === 'percentage' + ? subtotal * (state.coupon.value / 100) + : state.coupon.value; + } + + return { + ...state, + subtotal, + discount, + total: Math.max(0, subtotal - discount), + }; + } +} +``` + +## Testing + +```typescript +describe('Bloc', () => { + let bloc: CounterBloc; + + beforeEach(() => { + bloc = new CounterBloc(); + }); + + test('processes events', () => { + bloc.add(new Increment()); + expect(bloc.state).toBe(1); + + bloc.add(new Decrement()); + expect(bloc.state).toBe(0); + }); + + test('handles async events', async () => { + const dataBloc = new DataBloc(); + + dataBloc.add(new LoadData()); + expect(dataBloc.state.isLoading).toBe(true); + + await waitFor(() => { + expect(dataBloc.state.isLoading).toBe(false); + expect(dataBloc.state.data).toBeDefined(); + }); + }); + + test('queues events', async () => { + const events: string[] = []; + + bloc.on(ProcessStart, async (event, emit) => { + events.push('start'); + await sleep(10); + events.push('end'); + emit(state); + }); + + bloc.add(new ProcessStart()); + bloc.add(new ProcessStart()); + + await waitFor(() => { + expect(events).toEqual(['start', 'end', 'start', 'end']); + }); + }); +}); +``` + +## See Also + +- [Blocs Concept](/concepts/blocs) - Conceptual overview +- [BlocBase API](/api/core/bloc-base) - Parent class reference +- [Cubit API](/api/core/cubit) - Simpler alternative +- [useBloc Hook](/api/react/hooks#usebloc) - Using Blocs in React diff --git a/apps/docs/api/core/cubit.md b/apps/docs/api/core/cubit.md new file mode 100644 index 00000000..06c9e890 --- /dev/null +++ b/apps/docs/api/core/cubit.md @@ -0,0 +1,617 @@ +# Cubit Class + +The `Cubit` class is a simple state container that extends `BlocBase`. It provides direct state emission methods, making it perfect for straightforward state management scenarios. + +## Import + +```typescript +import { Cubit } from '@blac/core'; +``` + +## Class Definition + +```typescript +abstract class Cubit extends BlocBase +``` + +### Type Parameters + +| Parameter | Description | +| --------- | -------------------------------------- | +| `S` | The state type that this Cubit manages | + +## Constructor + +```typescript +constructor(initialState: S) +``` + +### Parameters + +| Parameter | Type | Description | +| -------------- | ---- | ----------------------- | +| `initialState` | `S` | The initial state value | + +### Example + +```typescript +class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); // Initial state is 0 + } +} + +class UserCubit extends Cubit { + constructor() { + super({ + user: null, + isLoading: false, + error: null, + }); + } +} +``` + +## Instance Methods + +### emit() + +Replaces the entire state with a new value. If the new state is identical to the current state (using `Object.is`), no update will occur. + +```typescript +emit(state: S): void +``` + +#### Parameters + +| Parameter | Type | Description | +| --------- | ---- | ------------------- | +| `state` | `S` | The new state value | + +#### Example + +```typescript +class ThemeCubit extends Cubit<{ theme: 'light' | 'dark' }> { + constructor() { + super({ theme: 'light' }); + } + + toggleTheme = () => { + this.emit({ theme: this.state.theme === 'light' ? 'dark' : 'light' }); + }; + + setTheme = (theme: 'light' | 'dark') => { + this.emit({ theme }); + }; +} +``` + +### patch() + +Partially updates the current state by merging it with the provided state patch. This method is only applicable when the state is an object type. If the state is not an object, a warning will be logged and no update will occur. + +```typescript +patch( + statePatch: S extends object ? Partial : S, + ignoreChangeCheck?: boolean +): void +``` + +#### Parameters + +| Parameter | Type | Description | +| ------------------- | ------------------- | ------------------------------------------------------- | +| `statePatch` | `Partial` or `S` | Partial state object (if S is object) or full state | +| `ignoreChangeCheck` | `boolean` | Skip equality check and force update (default: `false`) | + +#### Example + +```typescript +interface FormState { + name: string; + email: string; + age: number; + errors: Record; +} + +class FormCubit extends Cubit { + constructor() { + super({ + name: '', + email: '', + age: 0, + errors: {}, + }); + } + + // Update single field + updateName = (name: string) => { + this.patch({ name }); + }; + + // Update multiple fields + updateContact = (email: string, age: number) => { + this.patch({ email, age }); + }; + + // Force update even if values are same + forceRefresh = () => { + this.patch({}, true); + }; +} +``` + +## Inherited from BlocBase + +### Properties + +#### state + +The current state value. + +```typescript +get state(): S +``` + +Example: + +```typescript +class CounterCubit extends Cubit<{ count: number }> { + logState = () => { + console.log('Current count:', this.state.count); + }; +} +``` + +#### lastUpdate + +Timestamp of the last state update. + +```typescript +get lastUpdate(): number +``` + +Example: + +```typescript +class DataCubit extends Cubit { + get isStale() { + const fiveMinutes = 5 * 60 * 1000; + return Date.now() - this.lastUpdate > fiveMinutes; + } +} +``` + +### Static Properties + +#### isolated + +When `true`, each component gets its own instance. + +```typescript +static isolated: boolean = false +``` + +Example: + +```typescript +class FormCubit extends Cubit { + static isolated = true; // Each form component gets its own instance + + constructor() { + super({ fields: {} }); + } +} +``` + +#### keepAlive + +When `true`, instance persists even with no consumers. + +```typescript +static keepAlive: boolean = false +``` + +Example: + +```typescript +class SessionCubit extends Cubit { + static keepAlive = true; // Never dispose this instance + + constructor() { + super({ user: null }); + } +} +``` + +#### plugins + +Array of plugins to automatically attach to this Cubit class. + +```typescript +static plugins?: BlocPlugin[] +``` + +Example: + +```typescript +import { PersistencePlugin } from '@blac/persistence'; + +class SettingsCubit extends Cubit { + static plugins = [ + new PersistencePlugin({ + key: 'app-settings', + storage: localStorage, + }), + ]; + + constructor() { + super({ theme: 'light', language: 'en' }); + } +} +``` + +### Methods + +#### subscribe() + +Subscribe to state changes. + +```typescript +subscribe(callback: (state: S) => void): () => void +``` + +##### Parameters + +| Parameter | Type | Description | +| ---------- | -------------------- | -------------------------------- | +| `callback` | `(state: S) => void` | Function called on state changes | + +##### Returns + +An unsubscribe function that removes the subscription when called. + +##### Example + +```typescript +// External subscription +const cubit = new CounterCubit(); +const unsubscribe = cubit.subscribe((state) => { + console.log('Count changed to:', state.count); +}); + +// Later: cleanup +unsubscribe(); +``` + +#### subscribeWithSelector() + +Subscribe to state changes with a selector for optimized updates. + +```typescript +subscribeWithSelector( + selector: (state: S) => T, + callback: (value: T) => void, + equalityFn?: (a: T, b: T) => boolean +): () => void +``` + +##### Parameters + +| Parameter | Type | Description | +| ------------ | ------------------------- | ------------------------------------------------------ | +| `selector` | `(state: S) => T` | Function to select specific data from state | +| `callback` | `(value: T) => void` | Function called when selected value changes | +| `equalityFn` | `(a: T, b: T) => boolean` | Optional custom equality function (default: Object.is) | + +##### Example + +```typescript +const cubit = new UserCubit(); + +// Only notified when user name changes +const unsubscribe = cubit.subscribeWithSelector( + (state) => state.user?.name, + (name) => console.log('Name changed to:', name), +); +``` + +#### subscriptionCount + +Get the current number of active subscriptions. + +```typescript +get subscriptionCount(): number +``` + +##### Example + +```typescript +const cubit = new CounterCubit(); +console.log(cubit.subscriptionCount); // 0 + +const unsub1 = cubit.subscribe(() => {}); +const unsub2 = cubit.subscribe(() => {}); +console.log(cubit.subscriptionCount); // 2 + +unsub1(); +console.log(cubit.subscriptionCount); // 1 +``` + +#### onDispose() + +Override to perform cleanup when the instance is disposed. + +```typescript +protected onDispose(): void +``` + +Example: + +```typescript +class WebSocketCubit extends Cubit { + private ws?: WebSocket; + + connect = () => { + this.ws = new WebSocket('wss://api.example.com'); + // ... setup WebSocket + }; + + onDispose = () => { + this.ws?.close(); + console.log('WebSocket connection closed'); + }; +} +``` + +## Complete Examples + +### Counter Example + +```typescript +class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); + } + + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); + reset = () => this.emit({ count: 0 }); + + incrementBy = (amount: number) => { + this.emit({ count: this.state.count + amount }); + }; +} +``` + +### Async Data Fetching + +```typescript +interface DataState { + data: T | null; + isLoading: boolean; + error: string | null; +} + +class DataCubit extends Cubit> { + constructor() { + super({ + data: null, + isLoading: false, + error: null, + }); + } + + fetch = async (fetcher: () => Promise) => { + this.emit({ data: null, isLoading: true, error: null }); + + try { + const data = await fetcher(); + this.emit({ data, isLoading: false, error: null }); + } catch (error) { + this.emit({ + data: null, + isLoading: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + }; + + reset = () => { + this.emit({ data: null, isLoading: false, error: null }); + }; +} +``` + +### Form Management + +```typescript +interface FormField { + value: string; + error?: string; + touched: boolean; +} + +interface FormState { + fields: Record; + isSubmitting: boolean; + submitError?: string; +} + +class FormCubit extends Cubit { + static isolated = true; // Each form instance is independent + + constructor(private validator: FormValidator) { + super({ + fields: {}, + isSubmitting: false, + }); + } + + setField = (name: string, value: string) => { + const error = this.validator.validateField(name, value); + + this.patch({ + fields: { + ...this.state.fields, + [name]: { + value, + error, + touched: true, + }, + }, + }); + }; + + submit = async ( + onSubmit: (values: Record) => Promise, + ) => { + // Validate all fields + const errors = this.validator.validateAll(this.state.fields); + if (Object.keys(errors).length > 0) { + this.patch({ + fields: Object.entries(this.state.fields).reduce( + (acc, [name, field]) => ({ + ...acc, + [name]: { ...field, error: errors[name] }, + }), + {}, + ), + }); + return; + } + + // Submit + this.patch({ isSubmitting: true, submitError: undefined }); + + try { + const values = Object.entries(this.state.fields).reduce( + (acc, [name, field]) => ({ + ...acc, + [name]: field.value, + }), + {}, + ); + + await onSubmit(values); + + // Reset form on success + this.emit({ + fields: {}, + isSubmitting: false, + }); + } catch (error) { + this.patch({ + isSubmitting: false, + submitError: error instanceof Error ? error.message : 'Submit failed', + }); + } + }; + + get isValid() { + return Object.values(this.state.fields).every((field) => !field.error); + } + + get isDirty() { + return Object.values(this.state.fields).some((field) => field.touched); + } +} +``` + +## Testing + +Cubits are easy to test: + +```typescript +describe('CounterCubit', () => { + let cubit: CounterCubit; + + beforeEach(() => { + cubit = new CounterCubit(); + }); + + it('should start with initial state', () => { + expect(cubit.state).toEqual({ count: 0 }); + }); + + it('should increment', () => { + cubit.increment(); + expect(cubit.state).toEqual({ count: 1 }); + }); + + it('should notify subscribers on state changes', () => { + const listener = jest.fn(); + const unsubscribe = cubit.subscribe(listener); + + cubit.increment(); + + expect(listener).toHaveBeenCalledWith({ count: 1 }); + + unsubscribe(); + }); +}); +``` + +## Best Practices + +### 1. Use Arrow Functions + +Always use arrow functions for methods to maintain proper `this` binding: + +```typescript +// ✅ Good +increment = () => this.emit(this.state + 1); + +// ❌ Bad - loses 'this' context +increment() { + this.emit(this.state + 1); +} +``` + +### 2. Keep State Immutable + +Always create new state objects: + +```typescript +// ✅ Good +this.patch({ items: [...this.state.items, newItem] }); + +// ❌ Bad - mutating state +this.state.items.push(newItem); +this.emit(this.state); +``` + +### 3. Handle All States + +Consider loading, error, and success states: + +```typescript +type AsyncState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: string }; +``` + +### 4. Cleanup Resources + +Use `onDispose` for cleanup: + +```typescript +onDispose = () => { + this.subscription?.unsubscribe(); + this.timer && clearInterval(this.timer); +}; +``` + +## Summary + +Cubit provides: + +- **Simple API**: Just `emit()` and `patch()` for state updates +- **Type Safety**: Full TypeScript support with generics +- **Flexibility**: From simple counters to complex forms +- **Testability**: Easy to unit test in isolation +- **Lifecycle Management**: Automatic creation and disposal + +For more complex event-driven scenarios, consider using [Bloc](/api/core/bloc) instead. diff --git a/apps/docs/api/key-methods.md b/apps/docs/api/key-methods.md index 811199cc..a880d0e2 100644 --- a/apps/docs/api/key-methods.md +++ b/apps/docs/api/key-methods.md @@ -16,9 +16,9 @@ emit(state: S): void #### Parameters -| Name | Type | Description | -|------|------|-------------| -| `state` | `S` | The new state object that will replace the current state | +| Name | Type | Description | +| ------- | ---- | -------------------------------------------------------- | +| `state` | `S` | The new state object that will replace the current state | #### Example @@ -47,10 +47,10 @@ patch(statePatch: S extends object ? Partial : S, ignoreChangeCheck?: boolean #### Parameters -| Name | Type | Description | -|------|------|-------------| -| `statePatch` | `Partial` | An object containing the properties to update | -| `ignoreChangeCheck` | `boolean` | Optional flag to skip checking if values have changed (defaults to false) | +| Name | Type | Description | +| ------------------- | ------------ | ------------------------------------------------------------------------- | +| `statePatch` | `Partial` | An object containing the properties to update | +| `ignoreChangeCheck` | `boolean` | Optional flag to skip checking if values have changed (defaults to false) | #### Example @@ -66,7 +66,7 @@ class UserCubit extends Cubit<{ name: '', email: '', isLoading: false, - error: null + error: null, }); } @@ -85,112 +85,156 @@ class UserCubit extends Cubit<{ } ``` -## Action Handling +## Event Handling (Bloc) -### add(action) +### `on(eventConstructor, handler)` -The `add` method dispatches an action to the reducer function in a Bloc. +This method is specific to `Bloc` instances and is used to register a handler function for a specific type of event class. It should be called in the constructor. #### Signature ```tsx -add(action: A): void +protected on( + eventConstructor: new (...args: any[]) => E, + handler: (event: E, emit: (newState: S) => void) => void | Promise +): void ``` #### Parameters -| Name | Type | Description | -|------|------|-------------| -| `action` | `A` | The action to dispatch to the reducer | +| Name | Type | Description | +| ------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------ | +| `eventConstructor` | `new (...args: any[]) => E` | The constructor of the event class to listen for. | +| `handler` | `(event: InstanceType, emit: (newState: S) => void) => void` | A function that processes the event and can emit new states. | #### Example ```tsx -// Define actions -type TodoAction = - | { type: 'add', payload: { text: string } } - | { type: 'toggle', payload: { id: number } } - | { type: 'delete', payload: { id: number } }; +class MyEvent { + constructor(public data: string) {} +} +class AnotherEvent {} + +class MyBloc extends Bloc<{ value: string }, MyEvent | AnotherEvent> { + constructor() { + super({ value: 'initial' }); + + this.on(MyEvent, (event, emit) => { + emit({ value: `Handled MyEvent with: ${event.data}` }); + }); + + this.on(AnotherEvent, (_event, emit) => { + emit({ value: 'Handled AnotherEvent' }); + }); + } +} +``` + +### `add(event)` + +The `add` method dispatches an event instance. The `Bloc` will then look up and execute the handler registered for that event's specific class (constructor). +#### Signature + +```tsx +add(event: A): Promise // Where A is the union of event types the Bloc handles +``` + +#### Parameters + +| Name | Type | Description | +| ------- | ---- | ------------------------------- | +| `event` | `E` | The event instance to dispatch. | + +#### Example + +```tsx +// Define event classes +class AddTodoEvent { + constructor(public readonly text: string) {} +} +class ToggleTodoEvent { + constructor(public readonly id: number) {} +} + +// Define state interface TodoState { - todos: Array<{ id: number, text: string, completed: boolean }>; + todos: Array<{ id: number; text: string; completed: boolean }>; + nextId: number; } -class TodoBloc extends Bloc { +class TodoBloc extends Bloc { constructor() { - super({ todos: [] }); - } + super({ todos: [], nextId: 1 }); + + this.on(AddTodoEvent, (event, emit) => { + const newTodo = { + id: this.state.nextId, + text: event.text, + completed: false, + }; + emit({ + ...this.state, + todos: [...this.state.todos, newTodo], + nextId: this.state.nextId + 1, + }); + }); - // Implement the reducer - reducer = (action: TodoAction, state: TodoState) => { - switch (action.type) { - case 'add': - return { - todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }] - }; - case 'toggle': - return { - todos: state.todos.map((todo) => - todo.id === action.payload.id - ? { ...todo, completed: !todo.completed } - : todo - ) - }; - case 'delete': - return { - todos: state.todos.filter((todo) => todo.id !== action.payload.id) - }; - default: - return state; - } - }; + this.on(ToggleTodoEvent, (event, emit) => { + emit({ + ...this.state, + todos: this.state.todos.map((todo) => + todo.id === event.id ? { ...todo, completed: !todo.completed } : todo, + ), + }); + }); + } - // Helper methods to dispatch actions from the UI layer + // Helper methods to dispatch events from the UI layer (optional) addTodo = (text: string) => { - this.add({ type: 'add', payload: { text } }); + this.add(new AddTodoEvent(text)); }; toggleTodo = (id: number) => { - this.add({ type: 'toggle', payload: { id } }); - }; - - deleteTodo = (id: number) => { - this.add({ type: 'delete', payload: { id } }); + this.add(new ToggleTodoEvent(id)); }; } + +// Usage +const todoBloc = new TodoBloc(); +todoBloc.addTodo('Learn Blac Events'); +todoBloc.toggleTodo(1); ``` -## Subscription Management +## Subscription Management (BlocBase) -### on(event, listener, signal?) +### `subscribe(callback)` -The `on` method subscribes to events and returns an unsubscribe function. +The `subscribe` method subscribes to state changes and returns an unsubscribe function. #### Signature ```tsx -on(event: BlacEvent, listener: StateListener, signal?: AbortSignal): () => void +subscribe(callback: (state: S) => void): () => void ``` #### Parameters -| Name | Type | Description | -|------|------|-------------| -| `event` | `BlacEvent` | The event to listen to (e.g., BlacEvent.StateChange) | -| `listener` | `StateListener` | A function that will be called when the event occurs | -| `signal` | `AbortSignal` | An optional signal to abort the subscription | +| Name | Type | Description | +| ---------- | -------------------- | ------------------------------------------------- | +| `callback` | `(state: S) => void` | A function that will be called when state changes | #### Returns A function that, when called, unsubscribes the listener. -#### Example 1: Basic State Change Subscription +#### Example ```tsx const counterBloc = new CounterBloc(); // Subscribe to state changes -const unsubscribe = counterBloc.on(BlacEvent.StateChange, (state) => { +const unsubscribe = counterBloc.subscribe((state) => { console.log('State changed:', state); }); @@ -198,32 +242,40 @@ const unsubscribe = counterBloc.on(BlacEvent.StateChange, (state) => { unsubscribe(); ``` -#### Example 2: Using AbortController +### `subscribeWithSelector(selector, callback, equalityFn?)` -```tsx -const counterBloc = new CounterBloc(); -const abortController = new AbortController(); +Subscribe to state changes with a selector for optimized updates. -// Subscribe to state changes -counterBloc.on(BlacEvent.StateChange, (state) => { - console.log('State changed:', state); -}, abortController.signal); +#### Signature -// Abort the subscription -abortController.abort(); +```tsx +subscribeWithSelector( + selector: (state: S) => T, + callback: (value: T) => void, + equalityFn?: (a: T, b: T) => boolean +): () => void ``` -#### Example 3: Listening to Actions +#### Parameters + +| Name | Type | Description | +| ------------ | ------------------------- | ------------------------------------------------------ | +| `selector` | `(state: S) => T` | Function to select specific data from state | +| `callback` | `(value: T) => void` | Function called when selected value changes | +| `equalityFn` | `(a: T, b: T) => boolean` | Optional custom equality function (default: Object.is) | + +#### Example ```tsx const todoBloc = new TodoBloc(); -// Subscribe to actions -todoBloc.on(BlacEvent.Action, (state, oldState, action) => { - console.log('Action dispatched:', action); - console.log('Old state:', oldState); - console.log('New state:', state); -}); +// Only get notified when the todo count changes +const unsubscribe = todoBloc.subscribeWithSelector( + (state) => state.todos.length, + (count) => { + console.log('Todo count changed:', count); + }, +); ``` ## Choosing Between emit() and patch() @@ -231,7 +283,7 @@ todoBloc.on(BlacEvent.Action, (state, oldState, action) => { - Use `emit()` when you want to replace the entire state object, typically for simple states - Use `patch()` when you want to update specific properties without touching others, ideal for complex states -## Choosing Between Bloc and Cubit +## Choosing Between Bloc and Cubit -- Use `Bloc` for more complex state logic where an event-driven approach is beneficial. `Bloc`s process `Action`s (events) through a `reducer` function to produce new `State`. This pattern is similar to Redux reducers and is excellent for managing intricate state transitions and side effects in a structured way. -- Use `Cubit` for simpler state management scenarios where state changes can be triggered by direct method calls on the `Cubit` instance. These methods then use `emit()` or `patch()` to update the state. This direct approach is often more concise for straightforward cases and shares similarities with libraries like Zustand. \ No newline at end of file +- Use `Bloc` for more complex state logic where an event-driven approach is beneficial. `Bloc`s process specific event _classes_ through registered _handlers_ (using `this.on(EventType, handler)` and `this.add(new EventType())`) to produce new `State`. This pattern is excellent for managing intricate state transitions and side effects in a structured and type-safe way. +- Use `Cubit` for simpler state management scenarios where state changes can be triggered by direct method calls on the `Cubit` instance. These methods then use `emit()` or `patch()` to update the state. This direct approach is often more concise for straightforward cases. diff --git a/apps/docs/api/react-hooks.md b/apps/docs/api/react-hooks.md index 4dc7b8e2..51fd4746 100644 --- a/apps/docs/api/react-hooks.md +++ b/apps/docs/api/react-hooks.md @@ -11,38 +11,37 @@ The primary hook for connecting a component to a Bloc/Cubit. ```tsx function useBloc< B extends BlocConstructor, - O extends BlocHookOptions> ->( - blocClass: B, - options?: O -): [BlocState>, InstanceType] + O extends BlocHookOptions>, +>(blocClass: B, options?: O): [BlocState>, InstanceType]; ``` ### Parameters -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `blocClass` | `BlocConstructor` | Yes | The Bloc/Cubit class to use | -| `options.id` | `string` | No | Optional identifier for the Bloc/Cubit instance | -| `options.props` | `InferPropsFromGeneric` | No | Props to pass to the Bloc/Cubit constructor | -| `options.dependencySelector` | `BlocHookDependencyArrayFn>` | No | Function to select which state properties should trigger re-renders | -| `options.onMount` | `(bloc: InstanceType) => void` | No | Callback function invoked when the Bloc is mounted | +| Name | Type | Required | Description | +| ---------------------- | -------------------------------------- | -------- | -------------------------------------------------------------- | +| `blocClass` | `BlocConstructor` | Yes | The Bloc/Cubit class to use | +| `options.instanceId` | `string` | No | Optional identifier for the Bloc/Cubit instance | +| `options.staticProps` | `ConstructorParameters[0]` | No | Static props to pass to the Bloc/Cubit constructor | +| `options.dependencies` | `(bloc: InstanceType) => unknown[]` | No | Function to select dependencies that should trigger re-renders | +| `options.onMount` | `(bloc: InstanceType) => void` | No | Callback function invoked when the Bloc is mounted | ### Returns An array containing: `[state, bloc]` where: + 1. `state` is the current state of the Bloc/Cubit. 2. `bloc` is the Bloc/Cubit instance with methods. ### Examples #### Basic Usage + ```tsx function Counter() { // Basic usage const [state, bloc] = useBloc(CounterBloc); - + return (

Count: {state.count}

@@ -59,7 +58,7 @@ A major advantage of useBloc is its automatic tracking of property access. The h ```tsx function UserProfile() { const [state, bloc] = useBloc(UserProfileBloc); - + // This component will only re-render when state.name changes, // even if other properties in the state object change return

Welcome, {state.name}!

; @@ -68,24 +67,158 @@ function UserProfile() { In the example above, the component only accesses `state.name`, so changes to other properties like `state.email` or `state.settings` won't cause a re-render. +:::info Proxy Dependency Tracking +This automatic property tracking is enabled by default through BlaC's proxy-based dependency tracking system. You can disable it globally if needed: + +```tsx +import { Blac } from '@blac/core'; + +// Disable automatic tracking globally +Blac.setConfig({ proxyDependencyTracking: false }); + +// Now components will re-render on ANY state change +``` + +See the [Configuration](/api/configuration) page for more details. +::: + +--- + +#### Conditional Rendering and Getters with Automatic Tracking + +Blac's `useBloc` hook is smart enough to update its dependencies if the properties accessed in your component change due to conditional rendering or the use of getters. + +Let's consider a `UserProfileCubit` with a getter for a derived piece of information: + +```tsx +// cubits/UserProfileCubit.ts +import { Cubit } from '@blac/core'; + +interface UserProfileState { + firstName: string; + lastName: string; + age: number; + showFullName: boolean; + accessCount: number; // To demonstrate updates not affecting UI initially +} + +export class UserProfileCubit extends Cubit { + constructor() { + super({ + firstName: 'Jane', + lastName: 'Doe', + age: 30, + showFullName: true, + accessCount: 0, + }); + } + + // Getter for full name + get fullName(): string { + this.patch({ accessCount: this.state.accessCount + 1 }); // Side-effect for demo + return `${this.state.firstName} ${this.state.lastName}`; + } + + toggleShowFullName = () => + this.patch({ showFullName: !this.state.showFullName }); + setFirstName = (firstName: string) => this.patch({ firstName }); + setLastName = (lastName: string) => this.patch({ lastName }); + incrementAge = () => this.patch({ age: this.state.age + 1 }); + // Method to update a non-rendered property + incrementAccessCount = () => + this.patch({ accessCount: this.state.accessCount + 1 }); +} +``` + +Now, a component that uses this `UserProfileCubit` and conditionally renders information: + +```tsx +// components/UserProfileDemo.tsx +import React from 'react'; +import { useBloc } from '@blac/react'; +import { UserProfileCubit } from '../cubits/UserProfileCubit'; // Adjust path + +function UserProfileDemo() { + const [state, cubit] = useBloc(UserProfileCubit); + + console.log('UserProfileDemo re-rendered. Access count:', state.accessCount); + + return ( +
+

User Profile

+ {state.showFullName ? ( +

Name: {cubit.fullName}

// Accesses getter, which depends on firstName and lastName + ) : ( +

First Name: {state.firstName}

// Only accesses firstName + )} +

Age: {state.age}

+ + + + + + +
+ ); +} + +export default UserProfileDemo; +``` + +**Behavior Explanation:** + +1. **Initial Render (`showFullName` is `true`):** + - The component accesses `state.showFullName`, `cubit.fullName` (which in turn accesses `state.firstName` and `state.lastName` via the getter), and `state.age`. + - `useBloc` tracks these dependencies: `showFullName`, `firstName`, `lastName`, `age`. + - Changing `state.accessCount` via `cubit.incrementAccessCount()` will _not_ cause a re-render because `accessCount` is not directly used in the JSX. + +2. **Click "Toggle Full Name Display" (`showFullName` becomes `false`):** + - The component re-renders because `state.showFullName` changed. + - Now, the JSX accesses `state.showFullName`, `state.firstName`, and `state.age`. The `cubit.fullName` getter is no longer called. + - `useBloc` updates its dependency tracking. The new dependencies are: `showFullName`, `firstName`, `age`. + - **Crucially, `state.lastName` is no longer a dependency.** + +3. **After Toggling ( `showFullName` is `false`):** + - If you click "Set Last Name to Smith" (changing `state.lastName`), the component **will not re-render** because `state.lastName` is not currently an active dependency in the rendered output. + - If you click "Set First Name to John" (changing `state.firstName`), the component **will re-render** because `state.firstName` is an active dependency. + - If you click "Increment Age" (changing `state.age`), the component **will re-render**. + +4. **Toggle Back (`showFullName` becomes `true` again):** + - The component re-renders due to `state.showFullName` changing. + - The JSX now accesses `cubit.fullName` again. + - `useBloc` re-establishes `firstName` and `lastName` (via the getter) as dependencies, along with `showFullName` and `age`. + - Now, changing `state.lastName` will cause a re-render. + +This dynamic dependency tracking ensures optimal performance by only re-rendering your component when the state it _currently_ relies on for rendering actually changes. The use of getters is also seamlessly handled, with dependencies being tracked through the getter's own state access. + --- -#### Custom ID for Instance Management -If you want to share the same state between multiple components but need different instances of the Bloc/Cubit, you can define a custom ID. +#### Custom Instance ID for Instance Management + +If you want to share the same state between multiple components but need different instances of the Bloc/Cubit, you can define a custom instance ID. :::info By default, each Bloc/Cubit uses its class name as an identifier. ::: ```tsx -const [state, bloc] = useBloc(ChatThreadBloc, { id: 'thread-123' }); +const [state, bloc] = useBloc(ChatThreadBloc, { instanceId: 'thread-123' }); ``` + On its own, this is not very useful, but it becomes very powerful when the ID is dynamic. ```tsx function ChatThread({ conversationId }: { conversationId: string }) { - const [state, bloc] = useBloc(ChatThreadBloc, { - id: `thread-${conversationId}` + const [state, bloc] = useBloc(ChatThreadBloc, { + instanceId: `thread-${conversationId}`, }); return ( @@ -95,69 +228,113 @@ function ChatThread({ conversationId }: { conversationId: string }) { ); } ``` + With this approach, you can have multiple independent instances of state that share the same business logic. --- + #### Custom Dependency Selector -While property access is automatically tracked, in some cases you might want more control over when a component re-renders: + +While property access is automatically tracked, in some cases you might want more control over when a component re-renders. The dependencies function receives the bloc instance: + +:::tip Manual Dependencies Override Global Config +When you provide custom dependencies, it always takes precedence over the global `proxyDependencyTracking` setting. This allows you to have fine-grained control on a per-component basis regardless of global configuration. +::: ```tsx function OptimizedTodoList() { - // Using dependency selector for optimization + // Using custom dependencies for optimization const [state, bloc] = useBloc(TodoBloc, { - dependencySelector: (newState, oldState) => [ - newState.todos.length, - newState.filter - ] + dependencies: (instance) => [ + instance.state.todos.length, // Only track todo count + instance.state.filter, // Track filter changes + instance.hasUnsavedChanges, // Track computed property from bloc + ], }); - - // Component will only re-render when the length of todos or the filter changes + + // Component will only re-render when tracked dependencies change return (

Todos ({state.todos.length})

+

Filter: {state.filter}

{/* ... */}
); } ``` +#### Advanced Custom Selector Examples + +**Track only specific computed values:** + +```tsx +const [state, shoppingCart] = useBloc(ShoppingCartBloc, { + dependencies: (instance) => [ + instance.totalPrice, // Computed getter + instance.itemCount, // Another computed getter + instance.state.couponCode, // Specific state property + ], +}); +``` + +**Conditional dependency tracking:** + +```tsx +const [state, userBloc] = useBloc(UserBloc, { + dependencies: (instance) => { + const deps = [instance.state.isLoggedIn]; + + // Only track user details when logged in + if (instance.state.isLoggedIn) { + deps.push(instance.state.username, instance.state.email); + } + + return deps; + }, +}); +``` + +**Track message count changes:** + +```tsx +const [state, chatBloc] = useBloc(ChatBloc, { + dependencies: (instance) => [ + // Only re-render when the number of messages changes + instance.state.messages.length, + ], +}); +``` + ## Advanced Usage -### Props & Dependency Injection +### Static Props -Pass configuration to blocs during initialization. +Pass configuration to blocs during initialization using `staticProps`: ```tsx -// Bloc with props -class ThemeCubit extends Cubit { - constructor(props: ThemeProps) { - super({ theme: props.defaultTheme }); +// Bloc with constructor parameters +class ThemeCubit extends Cubit { + constructor(private config: { defaultTheme: string }) { + super({ theme: config.defaultTheme }); } } // In component function ThemeToggle() { - const [state, bloc] = useBloc(ThemeCubit, { - props: { defaultTheme: 'dark' } + const [state, bloc] = useBloc(ThemeCubit, { + staticProps: { defaultTheme: 'dark' }, }); - + return ( ); } -``` -If a prop changes, it will be updated in the Bloc/Cubit instance. This is useful for seamless integration with React components that use props to configure the Bloc/Cubit. -```tsx -function ThemeToggle(props: ThemeProps) { - const [state, bloc] = useBloc(ThemeCubit, { props }); - // ... -} - - ``` +**Note**: Static props are passed once during bloc creation and don't update if changed. + ### Initialization with onMount The `onMount` option provides a way to execute logic when a Bloc is mounted, allowing you to initialize or configure the Bloc without modifying its constructor: @@ -168,9 +345,9 @@ function UserProfile({ userId }: { userId: string }) { onMount: (bloc) => { // Load user data when the component mounts bloc.fetchUserData(userId); - } + }, }); - + return (
{state.isLoading ? ( @@ -193,44 +370,6 @@ The `onMount` callback runs once after the Bloc instance is created and the comp - Initializing the Bloc with component-specific data - Avoiding prop conflicts when multiple components use the same Bloc -:::warning -Make sure to only use the `props` option in the `useBloc` hook in a single place for each Bloc/Cubit. If there are multiple places that try to set the props, they might conflict with each other and cause unexpected behavior. -::: - -```tsx -function ThemeToggle() { - const [state, bloc] = useBloc(ThemeCubit, { - props: { defaultTheme: 'dark' } - }); - // ... -} - -function UseThemeToggle() { - const [state, bloc] = useBloc(ThemeCubit, { - props: { name: 'John' }// [!code error] - }); - // ... -} -``` - :::tip -If you need to pass props to a Bloc/Cubit that is not known at the time of initialization, you can use the `onMount` option. +Use `onMount` when you need to initialize a bloc with component-specific data or trigger initial data loading. ::: - -```tsx{10-12} -function ThemeToggle() { - const [state, bloc] = useBloc(ThemeCubit, { - props: { defaultTheme: 'dark' } - }); - // ... -} - -function UseThemeToggle() { - const [state, bloc] = useBloc(ThemeCubit, { - onMount: (bloc) => { - bloc.setName('John'); - } - }); - // ... -} -``` diff --git a/apps/docs/api/react/hooks.md b/apps/docs/api/react/hooks.md new file mode 100644 index 00000000..f8aecf6b --- /dev/null +++ b/apps/docs/api/react/hooks.md @@ -0,0 +1,546 @@ +# React Hooks API + +BlaC provides a set of React hooks that seamlessly integrate state management into your components. These hooks handle subscription, optimization, and lifecycle management automatically. + +## useBloc + +The primary hook for connecting React components to BlaC state containers. + +### Signature + +```typescript +function useBloc>>( + blocConstructor: B, + options?: { + staticProps?: ConstructorParameters[0]; + instanceId?: string; + dependencies?: (bloc: InstanceType) => unknown[]; + onMount?: (bloc: InstanceType) => void; + onUnmount?: (bloc: InstanceType) => void; + }, +): [BlocState>, InstanceType]; +``` + +### Type Parameters + +- `B` - The constructor type of your Cubit or Bloc class + +### Parameters + +- `BlocClass` - The constructor of your Cubit or Bloc class +- `options` - Optional configuration object + +### Options + +| Option | Type | Description | +| -------------- | ---------------------------------- | ----------------------------------------------------- | +| `instanceId` | `string` | Unique identifier for the instance | +| `staticProps` | Constructor's first parameter type | Props to pass to the constructor | +| `dependencies` | `(bloc: T) => unknown[]` | Function that returns values to track for re-creation | +| `onMount` | `(bloc: T) => void` | Called when the component mounts | +| `onUnmount` | `(bloc: T) => void` | Called when the component unmounts | + +### Returns + +Returns a tuple `[state, instance]`: + +- `state` - The current state (with automatic dependency tracking) +- `instance` - The Cubit/Bloc instance + +### Basic Usage + +```typescript +import { useBloc } from '@blac/react'; +import { CounterCubit } from './CounterCubit'; + +function Counter() { + const [count, cubit] = useBloc(CounterCubit); + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +### With Object State + +```typescript +function TodoList() { + const [state, cubit] = useBloc(TodoCubit); + + return ( +
+

Todos ({state.items.length})

+ {state.isLoading &&

Loading...

} + {state.items.map(todo => ( + + ))} +
+ ); +} +``` + +### Multiple Instances + +Use the `instanceId` option to create separate instances: + +```typescript +function Dashboard() { + const [user1] = useBloc(UserCubit, { instanceId: 'user-1' }); + const [user2] = useBloc(UserCubit, { instanceId: 'user-2' }); + + return ( +
+ + +
+ ); +} +``` + +### With Props + +Pass initialization props to your state container: + +```typescript +interface TodoListProps { + userId: string; + filter?: 'all' | 'active' | 'completed'; +} + +function TodoList({ userId, filter = 'all' }: TodoListProps) { + const [state, cubit] = useBloc(TodoCubit, { + instanceId: `todos-${userId}`, + staticProps: { userId, initialFilter: filter } + }); + + return
{/* ... */}
; +} +``` + +### Dependency Tracking + +By default, useBloc uses proxy-based dependency tracking for optimal re-renders: + +```typescript +function OptimizedComponent() { + const [state] = useBloc(LargeStateCubit); + + // Only re-renders when state.specificField changes + return
{state.specificField}
; +} +``` + +To disable proxy tracking globally (not recommended): + +```typescript +import { Blac } from '@blac/core'; + +// Disable for all components +Blac.setConfig({ proxyDependencyTracking: false }); +``` + +Or use manual dependencies for specific components: + +```typescript +const [state] = useBloc(CubitClass, { + dependencies: (bloc) => [bloc.state.specificField], // Manual dependency tracking +}); +``` + +### With Dependencies + +Re-create the instance when dependencies change: + +```typescript +function UserProfile({ userId }: { userId: string }) { + const [state, cubit] = useBloc(UserCubit, { + instanceId: `user-${userId}`, + staticProps: { userId }, + dependencies: () => [userId] // Re-create when userId changes + }); + + return
{state.user?.name}
; +} +``` + +## useExternalBlocStore + +A hook for using Bloc instances from external stores or dependency injection systems. + +### Signature + +```typescript +function useExternalBlocStore>( + externalBlocInstance: B, +): [BlocState, B]; +``` + +### Parameters + +- `externalBlocInstance` - An existing Bloc/Cubit instance from an external source + +### Returns + +Returns a tuple `[state, instance]` just like `useBloc` + +### Usage + +This hook is useful when you have Bloc instances managed by an external system: + +```typescript +// Using with dependency injection +function TodoListWithDI({ todoBloc }: { todoBloc: TodoCubit }) { + const [state, cubit] = useExternalBlocStore(todoBloc); + + return ( +
+ {state.items.map(todo => ( + + ))} +
+ ); +} + +// Using with a global store +const globalAuthBloc = new AuthBloc(); + +function AuthStatus() { + const [state] = useExternalBlocStore(globalAuthBloc); + return
Logged in: {state.isAuthenticated ? 'Yes' : 'No'}
; +} +``` + +## Hook Patterns + +### Conditional Usage + +```typescript +function ConditionalComponent({ showCounter }: { showCounter: boolean }) { + // ✅ Correct - always call hooks + const [count, cubit] = useBloc(CounterCubit); + + if (!showCounter) { + return null; + } + + return
Count: {count}
; +} + +// ❌ Wrong - conditional hook call +function BadComponent({ showCounter }: { showCounter: boolean }) { + if (showCounter) { + const [count] = useBloc(CounterCubit); // Error! + return
Count: {count}
; + } + return null; +} +``` + +### Custom Hooks + +Create custom hooks for complex logic: + +```typescript +function useAuth() { + const [state, bloc] = useBloc(AuthBloc); + + const login = useCallback( + (email: string, password: string) => { + bloc.login(email, password); + }, + [bloc] + ); + + const logout = useCallback(() => { + bloc.logout(); + }, [bloc]); + + return { + isAuthenticated: state.isAuthenticated, + user: state.user, + isLoading: state.isLoading, + error: state.error, + login, + logout + }; +} + +// Usage +function LoginButton() { + const { isAuthenticated, login, logout } = useAuth(); + + if (isAuthenticated) { + return ; + } + + return ; +} +``` + +### Combining Multiple States + +```typescript +function useAppState() { + const [auth] = useBloc(AuthBloc); + const [todos] = useBloc(TodoBloc); + const [settings] = useBloc(SettingsBloc); + + return { + isReady: auth.isAuthenticated && !todos.isLoading, + isDarkMode: settings.theme === 'dark', + userName: auth.user?.name, + }; +} +``` + +## Performance Optimization + +### Automatic Optimization + +BlaC automatically optimizes re-renders by tracking which state properties your component uses: + +```typescript +function UserCard() { + const [state] = useBloc(UserBloc); + + // Only re-renders when state.user.name changes + // Changes to other properties don't trigger re-renders + return

{state.user.name}

; +} +``` + +### Manual Optimization + +For fine-grained control, use React's built-in optimization: + +```typescript +const MemoizedTodoItem = React.memo(({ todo }: { todo: Todo }) => { + const [, cubit] = useBloc(TodoCubit); + + return ( +
+ {todo.text} + +
+ ); +}); +``` + +### Selector Pattern + +For complex derived state: + +```typescript +function TodoStats() { + const [state] = useBloc(TodoCubit); + + // Memoize expensive computations + const stats = useMemo(() => ({ + total: state.items.length, + completed: state.items.filter(t => t.completed).length, + active: state.items.filter(t => !t.completed).length + }), [state.items]); + + return ( +
+ Total: {stats.total} | + Active: {stats.active} | + Completed: {stats.completed} +
+ ); +} +``` + +## TypeScript Support + +### Type Inference + +BlaC hooks provide full type inference: + +```typescript +// State type is inferred +const [count, cubit] = useBloc(CounterCubit); +// count: number +// cubit: CounterCubit + +// With complex state +const [state, bloc] = useBloc(TodoBloc); +// state: TodoState +// bloc: TodoBloc +``` + +### Generic Constraints + +```typescript +// Custom hook with generic constraints +function useGenericBloc>>( + BlocClass: B, +) { + return useBloc(BlocClass); +} +``` + +### Typing Props + +```typescript +interface UserCubitProps { + userId: string; + initialData?: User; +} + +class UserCubit extends Cubit { + constructor(props: UserCubitProps) { + super({ user: props.initialData || null }); + } +} + +// Props are type-checked +const [state] = useBloc(UserCubit, { + staticProps: { + userId: '123', + // initialData is optional + }, +}); +``` + +## Common Patterns + +### Loading States + +```typescript +function DataComponent() { + const [state, cubit] = useBloc(DataCubit); + + useEffect(() => { + cubit.load(); + }, [cubit]); + + if (state.isLoading) return ; + if (state.error) return ; + if (!state.data) return ; + + return ; +} +``` + +### Form Handling + +```typescript +function LoginForm() { + const [state, cubit] = useBloc(LoginFormCubit); + + return ( +
{ e.preventDefault(); cubit.submit(); }}> + cubit.setEmail(e.target.value)} + className={state.email.error ? 'error' : ''} + /> + {state.email.error && {state.email.error}} + + +
+ ); +} +``` + +### Real-time Updates + +```typescript +function LiveData() { + const [state, cubit] = useBloc(LiveDataCubit); + + useEffect(() => { + // Subscribe to updates + const unsubscribe = cubit.subscribe(); + + // Cleanup + return () => { + unsubscribe(); + }; + }, [cubit]); + + return
Live value: {state.value}
; +} +``` + +## Troubleshooting + +### Instance Not Updating + +If your component doesn't update when state changes: + +1. Check that methods use arrow functions +2. Verify you're not mutating state +3. Ensure you're calling emit/patch correctly + +```typescript +// ❌ Wrong +class BadCubit extends Cubit { + update() { + // Regular method loses 'this' + this.state.value = 5; // Mutating state + } +} + +// ✅ Correct +class GoodCubit extends Cubit { + update = () => { + // Arrow function + this.emit({ ...this.state, value: 5 }); // New state + }; +} +``` + +### Memory Leaks + +BlaC automatically handles cleanup, but be careful with: + +```typescript +function Component() { + const [, cubit] = useBloc(TimerCubit); + + useEffect(() => { + const timer = setInterval(() => { + cubit.tick(); + }, 1000); + + // Important: cleanup + return () => clearInterval(timer); + }, [cubit]); +} +``` + +### Testing Components + +```typescript +import { renderHook } from '@testing-library/react-hooks'; +import { useBloc } from '@blac/react'; + +test('useBloc hook', () => { + const { result } = renderHook(() => useBloc(CounterCubit)); + + const [count, cubit] = result.current; + expect(count).toBe(0); + + act(() => { + cubit.increment(); + }); + + expect(result.current[0]).toBe(1); +}); +``` + +## See Also + +- [Instance Management](/concepts/instance-management) - How instances are managed +- [React Patterns](/react/patterns) - Best practices and patterns +- [Cubit API](/api/core/cubit) - Cubit class reference +- [Bloc API](/api/core/bloc) - Bloc class reference diff --git a/apps/docs/api/react/use-external-bloc-store.md b/apps/docs/api/react/use-external-bloc-store.md new file mode 100644 index 00000000..591e9cb0 --- /dev/null +++ b/apps/docs/api/react/use-external-bloc-store.md @@ -0,0 +1,392 @@ +# useExternalBlocStore + +A low-level React hook that provides an external store interface for use with React's `useSyncExternalStore`. This hook is primarily for advanced use cases and library integrations. + +## Overview + +`useExternalBlocStore` creates an external store interface compatible with React 18's `useSyncExternalStore` API. This is useful for: + +- Building custom hooks on top of BlaC +- Integration with third-party state management tools +- Advanced performance optimizations +- Library authors extending BlaC functionality + +## Signature + +```typescript +function useExternalBlocStore>>( + blocConstructor: B, + options?: { + id?: string; + staticProps?: ConstructorParameters[0]; + selector?: ( + currentState: BlocState>, + previousState: BlocState>, + instance: InstanceType, + ) => any[]; + }, +): { + externalStore: ExternalStore>>; + instance: { current: InstanceType | null }; + usedKeys: { current: Set }; + usedClassPropKeys: { current: Set }; + rid: string; +}; +``` + +## Parameters + +| Name | Type | Required | Description | +| --------------------- | --------------------------- | -------- | -------------------------------------------------- | +| `blocConstructor` | `B extends BlocConstructor` | Yes | The bloc class constructor | +| `options.id` | `string` | No | Unique identifier for the instance | +| `options.staticProps` | Constructor params | No | Props to pass to the constructor | +| `options.selector` | `Function` | No | Custom dependency selector for render optimization | + +## Returns + +Returns an object containing: + +- `externalStore`: An external store interface with `getSnapshot`, `subscribe`, and `getServerSnapshot` methods +- `instance`: A ref containing the bloc instance +- `usedKeys`: A ref tracking used state keys +- `usedClassPropKeys`: A ref tracking used class property keys +- `rid`: A unique render ID + +## Basic Usage + +### Using with useSyncExternalStore + +```typescript +import { useSyncExternalStore } from 'react'; +import { useExternalBlocStore } from '@blac/react'; +import { CounterCubit } from './CounterCubit'; + +function Counter() { + const { externalStore, instance } = useExternalBlocStore(CounterCubit); + + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + externalStore.getServerSnapshot + ); + + return ( +
+

Count: {state?.count ?? 0}

+ +
+ ); +} +``` + +## Advanced Usage + +### Building a Custom Hook + +```typescript +import { useSyncExternalStore } from 'react'; +import { useExternalBlocStore } from '@blac/react'; +import { BlocConstructor, BlocBase } from '@blac/core'; + +// Custom hook using external store +function useSimpleBloc>>( + blocConstructor: B, + options?: Parameters[1] +) { + const { externalStore, instance } = useExternalBlocStore( + blocConstructor, + options + ); + + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + externalStore.getServerSnapshot + ); + + return [state, instance.current] as const; +} + +// Usage +function TodoList() { + const [state, cubit] = useSimpleBloc(TodoCubit, { + selector: (state) => [state.items.length] + }); + + return
Todos: {state?.items.length ?? 0}
; +} +``` + +### With Selector for Optimization + +```typescript +function useOptimizedBloc>>( + blocConstructor: B, + selector: (state: any) => any +) { + const { externalStore, instance } = useExternalBlocStore( + blocConstructor, + { + selector: (currentState, previousState, bloc) => { + // Track dependencies for optimization + return [selector(currentState)]; + } + } + ); + + const state = useSyncExternalStore( + externalStore.subscribe, + () => { + const snapshot = externalStore.getSnapshot(); + return snapshot ? selector(snapshot) : undefined; + }, + () => { + const snapshot = externalStore.getServerSnapshot?.(); + return snapshot ? selector(snapshot) : undefined; + } + ); + + return [state, instance.current] as const; +} + +// Usage - only re-renders when count changes +function CountDisplay() { + const [count] = useOptimizedBloc( + ComplexStateCubit, + state => state.metrics.count + ); + + return
Count: {count}
; +} +``` + +### Integration with State Libraries + +```typescript +// Integrate with Zustand, Valtio, or other state libraries +import { create } from 'zustand'; +import { useExternalBlocStore } from '@blac/react'; +import { useSyncExternalStore } from 'react'; + +interface StoreState { + bloc: CounterCubit | null; + initializeBloc: () => void; +} + +const useStore = create((set) => ({ + bloc: null, + initializeBloc: () => { + const { instance } = useExternalBlocStore(CounterCubit); + set({ bloc: instance.current }); + }, +})); + +// Component using the integrated store +function Counter() { + const bloc = useStore(state => state.bloc); + const initializeBloc = useStore(state => state.initializeBloc); + + useEffect(() => { + if (!bloc) initializeBloc(); + }, [bloc, initializeBloc]); + + if (!bloc) return null; + + // Use the bloc with external store + const { externalStore } = useExternalBlocStore(CounterCubit); + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot + ); + + return
Count: {state?.count ?? 0}
; +} +``` + +## Best Practices + +### 1. Use useBloc Instead + +For most use cases, prefer the higher-level `useBloc` hook: + +```typescript +// ✅ Preferred for most cases +const [state, cubit] = useBloc(CounterCubit); + +// ⚠️ Only use external store for advanced cases +const { externalStore } = useExternalBlocStore(CounterCubit); +``` + +### 2. Proper Instance Management + +The external store creates and manages bloc instances: + +```typescript +function MyComponent() { + // Instance is created and managed by the hook + const { instance } = useExternalBlocStore(CounterCubit, { + id: 'my-counter', + staticProps: { initialCount: 0 } + }); + + // Access the instance via ref + const handleClick = () => { + instance.current?.increment(); + }; + + return ; +} +``` + +### 3. Server-Side Rendering + +The external store provides SSR support: + +```typescript +function SSRComponent() { + const { externalStore } = useExternalBlocStore(DataCubit); + + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + externalStore.getServerSnapshot // SSR support + ); + + return
{state?.data}
; +} +``` + +## Common Patterns + +### Custom Hook Library + +```typescript +// Build a library of custom hooks +export function createBlocHook>>( + blocConstructor: B, +) { + return function useCustomBloc( + options?: Parameters[1], + ) { + const { externalStore, instance } = useExternalBlocStore( + blocConstructor, + options, + ); + + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + externalStore.getServerSnapshot, + ); + + return [state, instance.current] as const; + }; +} + +// Create specific hooks +export const useCounter = createBlocHook(CounterCubit); +export const useTodos = createBlocHook(TodoCubit); +export const useAuth = createBlocHook(AuthCubit); +``` + +### Performance Monitoring + +```typescript +// Track render performance +function useMonitoredBloc>>( + blocConstructor: B, +) { + const renderCount = useRef(0); + const { externalStore, instance, usedKeys } = + useExternalBlocStore(blocConstructor); + + useEffect(() => { + renderCount.current++; + console.log(`Render #${renderCount.current}`, { + blocName: blocConstructor.name, + usedKeys: Array.from(usedKeys.current), + }); + }); + + const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + ); + + return [state, instance.current] as const; +} +``` + +## Comparison with useBloc + +| Feature | useBloc | useExternalBlocStore | +| -------------------- | ----------------- | --------------------- | +| Level of abstraction | High-level | Low-level | +| Use with | Direct usage | useSyncExternalStore | +| Return value | [state, instance] | External store object | +| Lifecycle management | Automatic | Automatic | +| Props support | Yes | Yes | +| Best for | Most use cases | Library authors | + +## Troubleshooting + +### TypeScript Errors + +Ensure proper type inference: + +```typescript +// ❌ Type errors with generic constraints +const { externalStore } = useExternalBlocStore(CounterCubit); + +// ✅ Let TypeScript infer types +const { externalStore } = useExternalBlocStore(CounterCubit); +``` + +### Missing State Updates + +Check subscription setup: + +```typescript +// ❌ Forgetting to use the subscribe method +const state = externalStore.getSnapshot(); + +// ✅ Proper subscription with useSyncExternalStore +const state = useSyncExternalStore( + externalStore.subscribe, + externalStore.getSnapshot, + externalStore.getServerSnapshot, +); +``` + +## When to Use This Hook + +Use `useExternalBlocStore` when: + +1. Building custom React hooks on top of BlaC +2. Integrating with React 18's concurrent features +3. Creating a state management library wrapper +4. Need fine-grained control over subscriptions +5. Implementing server-side rendering with hydration + +For standard application development, use the `useBloc` hook instead. + +## API Reference + +### ExternalStore Interface + +```typescript +interface ExternalStore { + getSnapshot: () => T | undefined; + subscribe: (listener: () => void) => () => void; + getServerSnapshot?: () => T | undefined; +} +``` + +## Next Steps + +- [useBloc](/api/react/hooks#usebloc) - High-level hook for most use cases +- [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) - React documentation +- [Instance Management](/concepts/instance-management) - Learn about bloc lifecycle diff --git a/apps/docs/blog/inspiration.md b/apps/docs/blog/inspiration.md index 9dce8988..752451f1 100644 --- a/apps/docs/blog/inspiration.md +++ b/apps/docs/blog/inspiration.md @@ -1,5 +1,5 @@ - ## Inspiration + > "standing on the shoulders of giants" ### BLoC Pattern @@ -15,15 +15,19 @@ Although Blac is framework agnostic, it was initially created to be used with Re > Although I have some frustrations with React, I still love working with it and use it for most of my projects. This list is not meant to be a critique, but rather an explanation of why Blac was created. #### useState + The bread and butter of React state management, useState is great for managing the state of a single component, as long as the component is very simple and the state is not shared with other components. During development, I often find myself reaching the limit of what I am comfortable with useState, usually when I reach a unspecific amount of `useState` calls and my component becomes cluttered with `setState` calls. #### useEffect + Using `useEffect` for lifecycle management is confusing and error prone, not to mention the pain it is to test with unit tests. Handling side effects is incredibly error prone and hard to reason about. #### Context API + Although the Context API is a godsend to avoid prop drilling and setting the context for a branch in the render tree. When overused it can lead to a tight coupling between components and make it hard to move or reuse components outside of the current context tree. #### Component Props + Most of the props passed to components are references to the business logic and state of the component. This is mostly just boilerplate to pass the state and callbacks to the component. When refactoring, updating props takes up a lot of time and causes a lot of noise in the code. diff --git a/apps/docs/comparisons.md b/apps/docs/comparisons.md new file mode 100644 index 00000000..701b3edf --- /dev/null +++ b/apps/docs/comparisons.md @@ -0,0 +1,102 @@ +# Comparison with Other Solutions + +BlaC takes a unique approach to state management by focusing on separation of concerns and developer experience. Here's how it compares to popular alternatives: + +## vs Redux + +**Community Pain Points**: Developers report having to "touch five different files just to make one change" and struggle with TypeScript integration requiring separate type definitions for actions, action types, and objects. + +**How BlaC Addresses These**: + +- **Minimal boilerplate**: One class with methods that call `emit()` - no actions, action creators, or reducers +- **Automatic TypeScript inference**: State types flow naturally from your class definition without manual type annotations +- **No Redux Toolkit learning curve**: Simple API that doesn't require learning additional abstractions + +## vs MobX + +**Community Pain Points**: Observable arrays aren't real arrays (breaks with lodash, Array.concat), can't make primitive values observable, dynamic property additions require special handling, and debugging automatic reactions can be challenging. + +**How BlaC Addresses These**: + +- **Standard JavaScript objects**: Your state is plain JS/TS - no special array types or observable primitives to worry about +- **Predictable updates**: Explicit `emit()` calls make state changes traceable through your codebase +- **No "magic" to debug**: While MobX uses proxies for automatic reactivity, BlaC only uses them for render optimization + +## vs Context + useReducer + +**Community Pain Points**: Any context change re-renders ALL consuming components (even if they only use part of the state), no built-in async support, and complex apps require extensive memoization to prevent performance issues. + +**How BlaC Addresses These**: + +- **Automatic render optimization**: Only re-renders components that use the specific properties that changed +- **Built-in async patterns**: Handle async operations naturally in your state container methods +- **No manual memoization needed**: Performance optimization happens automatically without useMemo/useCallback +- **No context providers**: Any component can access any state container without needing to wrap it in a provider + +## vs Zustand/Valtio + +**Community Pain Points**: Zustand requires manual selectors for each component usage, both are designed for module state (not component state), and mixing mutable (Valtio) with React's immutable model can cause confusion. + +**How BlaC Addresses These**: + +- **Flexible state patterns**: Use `isolated` for component-specific state or share state across components +- **Clear architectural patterns**: Cubit for simple cases, Bloc for complex event-driven scenarios +- **Consistent mental model**: Always use explicit updates, matching React's immutable state philosophy + +## Quick Comparison Table + +| Feature | Redux | MobX | Context API | Zustand | BlaC | +| ------------------ | ------------------- | --------- | ------------------ | ---------------- | --------- | +| **Boilerplate** | High | Low | Medium | Low | Low | +| **TypeScript** | Manual | Good | Good | Good | Automatic | +| **Async Support** | Redux-Thunk/Saga | Built-in | Manual | Manual | Built-in | +| **Performance** | Manual optimization | Automatic | Manual memoization | Manual selectors | Automatic | +| **Learning Curve** | Steep | Moderate | Low | Low | Low | +| **DevTools** | Excellent | Good | Basic | Good | Good | +| **Testing** | Complex | Moderate | Complex | Simple | Simple | +| **Code Splitting** | Manual | Automatic | N/A | Manual | Automatic | + +## When to Choose BlaC + +Choose BlaC when you want: + +- **Clean architecture** with separated business logic +- **Minimal boilerplate** without sacrificing power +- **Automatic performance optimization** without manual work +- **First-class TypeScript support** with zero type annotations +- **Flexible patterns** that scale from simple to complex apps +- **Easy testing** with isolated business logic + +## Migration Strategies + +### From Redux + +1. Start by converting one Redux slice to a Cubit +2. Use BlaC's event-driven Bloc pattern for complex flows +3. Gradually migrate feature by feature +4. Keep Redux DevTools integration with BlaC's plugin system + +### From MobX + +1. Convert observable classes to Cubits +2. Replace reactions with explicit method calls +3. Use computed getters for derived state +4. Maintain the same component structure + +### From Context API + +1. Extract context logic into Cubits +2. Replace useContext with useBloc +3. Remove Provider components +4. Enjoy automatic performance benefits + +### From Zustand + +1. Convert stores to Cubits (very similar API) +2. Remove manual selectors +3. Use instance management for component state +4. Keep the same mental model + +## Summary + +BlaC provides a modern approach to state management that addresses common pain points while maintaining simplicity. It's not trying to be a drop-in replacement for other solutions, but rather a better way to structure your application's state management from the ground up. diff --git a/apps/docs/concepts/blocs.md b/apps/docs/concepts/blocs.md new file mode 100644 index 00000000..84bb317d --- /dev/null +++ b/apps/docs/concepts/blocs.md @@ -0,0 +1,694 @@ +# Blocs + +Blocs provide event-driven state management for complex scenarios. While Cubits offer direct state updates, Blocs use events and handlers for more structured, traceable state transitions. + +## What is a Bloc? + +A Bloc (Business Logic Component) is a state container that: + +- Processes events through registered handlers +- Maintains a clear separation between events and logic +- Provides better debugging through event history +- Scales well for complex state management + +## Bloc vs Cubit + +### When to use Cubit: + +- Simple state with straightforward updates +- Direct method calls are sufficient +- Quick prototyping +- Small, focused features + +### When to use Bloc: + +- Complex business logic with many state transitions +- Need for event history and debugging +- Multiple triggers for the same state change +- Team collaboration benefits from explicit events + +## Creating a Bloc + +### Basic Structure + +```typescript +import { Bloc } from '@blac/core'; + +// 1. Define your events as classes +class CounterIncremented { + constructor(public readonly amount: number = 1) {} +} + +class CounterDecremented { + constructor(public readonly amount: number = 1) {} +} + +class CounterReset {} + +// 2. Create a union type of all events (optional but helpful) +type CounterEvent = CounterIncremented | CounterDecremented | CounterReset; + +// 3. Create your Bloc +class CounterBloc extends Bloc { + constructor() { + super(0); // Initial state + + // Register event handlers + this.on(CounterIncremented, this.handleIncrement); + this.on(CounterDecremented, this.handleDecrement); + this.on(CounterReset, this.handleReset); + } + + // Event handlers + private handleIncrement = ( + event: CounterIncremented, + emit: (state: number) => void, + ) => { + emit(this.state + event.amount); + }; + + private handleDecrement = ( + event: CounterDecremented, + emit: (state: number) => void, + ) => { + emit(this.state - event.amount); + }; + + private handleReset = ( + _event: CounterReset, + emit: (state: number) => void, + ) => { + emit(0); + }; + + // Public methods for convenience + increment = (amount = 1) => this.add(new CounterIncremented(amount)); + decrement = (amount = 1) => this.add(new CounterDecremented(amount)); + reset = () => this.add(new CounterReset()); +} +``` + +## Event Design + +### Event Classes + +Events should be: + +- **Immutable**: Use `readonly` properties +- **Descriptive**: Name indicates what happened +- **Data-carrying**: Include all necessary information + +```typescript +// ✅ Good event design +class UserLoggedIn { + constructor( + public readonly userId: string, + public readonly timestamp: Date, + public readonly sessionId: string, + ) {} +} + +class ProductAddedToCart { + constructor( + public readonly productId: string, + public readonly quantity: number, + public readonly price: number, + ) {} +} + +// ❌ Poor event design +class UpdateUser { + constructor(public data: any) {} // Too generic +} + +class Event { + type: string; // Stringly typed + payload: unknown; // No type safety +} +``` + +### Event Naming + +Use past tense to indicate something that happened: + +```typescript +// ✅ Good naming +class OrderPlaced {} +class PaymentProcessed {} +class UserRegistered {} + +// ❌ Poor naming +class PlaceOrder {} // Sounds like a command +class ProcessPayment {} // Not clear if it happened +class RegisterUser {} // Ambiguous +``` + +## Complex Example: Shopping Cart + +Let's build a full-featured shopping cart to showcase Bloc patterns: + +```typescript +// Events +class ItemAddedToCart { + constructor( + public readonly productId: string, + public readonly name: string, + public readonly price: number, + public readonly quantity: number = 1, + ) {} +} + +class ItemRemovedFromCart { + constructor(public readonly productId: string) {} +} + +class QuantityUpdated { + constructor( + public readonly productId: string, + public readonly quantity: number, + ) {} +} + +class CartCleared {} + +class DiscountApplied { + constructor( + public readonly code: string, + public readonly percentage: number, + ) {} +} + +class DiscountRemoved {} + +class CheckoutStarted {} + +class CheckoutCompleted { + constructor(public readonly orderId: string) {} +} + +// State +interface CartItem { + productId: string; + name: string; + price: number; + quantity: number; +} + +interface CartState { + items: CartItem[]; + discount: { + code: string; + percentage: number; + } | null; + status: 'shopping' | 'checking-out' | 'completed'; + orderId?: string; +} + +// Bloc +class CartBloc extends Bloc { + constructor(private api: CartAPI) { + super({ + items: [], + discount: null, + status: 'shopping', + }); + + // Register handlers + this.on(ItemAddedToCart, this.handleItemAdded); + this.on(ItemRemovedFromCart, this.handleItemRemoved); + this.on(QuantityUpdated, this.handleQuantityUpdated); + this.on(CartCleared, this.handleCartCleared); + this.on(DiscountApplied, this.handleDiscountApplied); + this.on(DiscountRemoved, this.handleDiscountRemoved); + this.on(CheckoutStarted, this.handleCheckoutStarted); + this.on(CheckoutCompleted, this.handleCheckoutCompleted); + } + + // Handlers + private handleItemAdded = ( + event: ItemAddedToCart, + emit: (state: CartState) => void, + ) => { + const existingItem = this.state.items.find( + (item) => item.productId === event.productId, + ); + + if (existingItem) { + // Update quantity if item exists + emit({ + ...this.state, + items: this.state.items.map((item) => + item.productId === event.productId + ? { ...item, quantity: item.quantity + event.quantity } + : item, + ), + }); + } else { + // Add new item + emit({ + ...this.state, + items: [ + ...this.state.items, + { + productId: event.productId, + name: event.name, + price: event.price, + quantity: event.quantity, + }, + ], + }); + } + + // Save to backend + this.api.saveCart(this.state.items); + }; + + private handleItemRemoved = ( + event: ItemRemovedFromCart, + emit: (state: CartState) => void, + ) => { + emit({ + ...this.state, + items: this.state.items.filter( + (item) => item.productId !== event.productId, + ), + }); + }; + + private handleQuantityUpdated = ( + event: QuantityUpdated, + emit: (state: CartState) => void, + ) => { + if (event.quantity <= 0) { + // Remove item if quantity is 0 or less + this.add(new ItemRemovedFromCart(event.productId)); + return; + } + + emit({ + ...this.state, + items: this.state.items.map((item) => + item.productId === event.productId + ? { ...item, quantity: event.quantity } + : item, + ), + }); + }; + + private handleDiscountApplied = async ( + event: DiscountApplied, + emit: (state: CartState) => void, + ) => { + try { + // Validate discount code + const isValid = await this.api.validateDiscount(event.code); + + if (isValid) { + emit({ + ...this.state, + discount: { + code: event.code, + percentage: event.percentage, + }, + }); + } + } catch (error) { + // Handle invalid discount + console.error('Invalid discount code:', error); + } + }; + + private handleCheckoutStarted = async ( + _event: CheckoutStarted, + emit: (state: CartState) => void, + ) => { + emit({ + ...this.state, + status: 'checking-out', + }); + + try { + const orderId = await this.api.createOrder(this.state); + this.add(new CheckoutCompleted(orderId)); + } catch (error) { + // Revert to shopping status on error + emit({ + ...this.state, + status: 'shopping', + }); + throw error; + } + }; + + private handleCheckoutCompleted = ( + event: CheckoutCompleted, + emit: (state: CartState) => void, + ) => { + emit({ + items: [], + discount: null, + status: 'completed', + orderId: event.orderId, + }); + }; + + // Computed values + get subtotal() { + return this.state.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + } + + get discountAmount() { + if (!this.state.discount) return 0; + return this.subtotal * (this.state.discount.percentage / 100); + } + + get total() { + return this.subtotal - this.discountAmount; + } + + get itemCount() { + return this.state.items.reduce((sum, item) => sum + item.quantity, 0); + } + + // Public methods + addItem = (product: Product, quantity = 1) => { + this.add( + new ItemAddedToCart(product.id, product.name, product.price, quantity), + ); + }; + + removeItem = (productId: string) => { + this.add(new ItemRemovedFromCart(productId)); + }; + + updateQuantity = (productId: string, quantity: number) => { + this.add(new QuantityUpdated(productId, quantity)); + }; + + applyDiscount = (code: string, percentage: number) => { + this.add(new DiscountApplied(code, percentage)); + }; + + checkout = () => { + if (this.state.items.length === 0) { + throw new Error('Cannot checkout with empty cart'); + } + this.add(new CheckoutStarted()); + }; +} +``` + +## Advanced Patterns + +### Event Transformation + +Transform one event into another: + +```typescript +class DataBloc extends Bloc { + constructor() { + super(initialState); + + this.on(RefreshRequested, this.handleRefresh); + this.on(DataFetched, this.handleDataFetched); + } + + private handleRefresh = ( + event: RefreshRequested, + emit: (state: DataState) => void, + ) => { + // Transform refresh into fetch + this.add(new DataFetched(event.force)); + }; +} +``` + +### Debouncing Events + +Prevent rapid event firing: + +```typescript +class SearchBloc extends Bloc { + private searchDebounce?: NodeJS.Timeout; + + constructor() { + super({ query: '', results: [], isSearching: false }); + + this.on(SearchQueryChanged, this.handleQueryChanged); + this.on(SearchExecuted, this.handleSearchExecuted); + } + + private handleQueryChanged = ( + event: SearchQueryChanged, + emit: (state: SearchState) => void, + ) => { + emit({ ...this.state, query: event.query }); + + // Debounce search execution + if (this.searchDebounce) { + clearTimeout(this.searchDebounce); + } + + this.searchDebounce = setTimeout(() => { + this.add(new SearchExecuted(event.query)); + }, 300); + }; +} +``` + +### Event Logging + +Use plugins to track all events for debugging: + +```typescript +import { BlacPlugin } from '@blac/core'; + +class EventLoggingPlugin implements BlacPlugin { + name = 'EventLoggingPlugin'; + version = '1.0.0'; + + onEventAdded(bloc: Bloc, event: any) { + console.log(`[${bloc._name}]`, { + event: event.constructor.name, + data: event, + timestamp: new Date().toISOString(), + }); + } +} + +// Add to specific bloc or globally +Blac.instance.plugins.add(new EventLoggingPlugin()); +``` + +### Async Event Sequences + +Handle complex async flows: + +```typescript +class FileUploadBloc extends Bloc { + constructor() { + super({ files: [], uploading: false, progress: 0 }); + + this.on(FilesSelected, this.handleFilesSelected); + this.on(UploadStarted, this.handleUploadStarted); + this.on(UploadProgress, this.handleUploadProgress); + this.on(UploadCompleted, this.handleUploadCompleted); + this.on(UploadFailed, this.handleUploadFailed); + } + + private handleFilesSelected = async ( + event: FilesSelected, + emit: (state: FileUploadState) => void, + ) => { + emit({ ...this.state, files: event.files }); + + // Start upload automatically + this.add(new UploadStarted()); + }; + + private handleUploadStarted = async ( + event: UploadStarted, + emit: (state: FileUploadState) => void, + ) => { + emit({ ...this.state, uploading: true, progress: 0 }); + + try { + for (let i = 0; i < this.state.files.length; i++) { + const file = this.state.files[i]; + + // Upload with progress + await this.uploadFile(file, (progress) => { + this.add(new UploadProgress(progress)); + }); + } + + this.add(new UploadCompleted()); + } catch (error) { + this.add(new UploadFailed(error.message)); + } + }; +} +``` + +## Testing Blocs + +Blocs are highly testable due to their event-driven nature: + +```typescript +describe('CartBloc', () => { + let bloc: CartBloc; + let mockApi: jest.Mocked; + + beforeEach(() => { + mockApi = createMockCartAPI(); + bloc = new CartBloc(mockApi); + }); + + describe('adding items', () => { + it('should add new item to empty cart', async () => { + // Arrange + const product = { + id: '123', + name: 'Test Product', + price: 29.99, + }; + + // Act + bloc.add(new ItemAddedToCart(product.id, product.name, product.price, 1)); + + // Assert + expect(bloc.state.items).toHaveLength(1); + expect(bloc.state.items[0]).toEqual({ + productId: '123', + name: 'Test Product', + price: 29.99, + quantity: 1, + }); + }); + + it('should increase quantity for existing item', async () => { + // Arrange - add item first + bloc.add(new ItemAddedToCart('123', 'Test', 10, 1)); + + // Act - add same item again + bloc.add(new ItemAddedToCart('123', 'Test', 10, 2)); + + // Assert + expect(bloc.state.items).toHaveLength(1); + expect(bloc.state.items[0].quantity).toBe(3); + }); + }); + + describe('checkout flow', () => { + it('should complete checkout successfully', async () => { + // Arrange + bloc.add(new ItemAddedToCart('123', 'Test', 10, 1)); + mockApi.createOrder.mockResolvedValue('order-123'); + + // Act + bloc.add(new CheckoutStarted()); + + // Wait for async operations + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Assert + expect(bloc.state.status).toBe('completed'); + expect(bloc.state.orderId).toBe('order-123'); + expect(bloc.state.items).toHaveLength(0); + }); + }); +}); +``` + +## Best Practices + +### 1. Keep Events Simple + +Events should only carry data, not logic: + +```typescript +// ✅ Good +class TodoAdded { + constructor(public readonly text: string) {} +} + +// ❌ Bad +class TodoAdded { + constructor(public readonly text: string) { + if (!text) throw new Error('Text required'); // Logic in event + } +} +``` + +### 2. Handler Purity + +Handlers should be pure functions (except for emit): + +```typescript +// ✅ Good +private handleIncrement = (event: Increment, emit: (state: State) => void) => { + emit({ count: this.state.count + event.amount }); +}; + +// ❌ Bad +private handleIncrement = (event: Increment, emit: (state: State) => void) => { + localStorage.setItem('count', this.state.count); // Side effect! + window.alert('Incremented!'); // Side effect! + emit({ count: this.state.count + event.amount }); +}; +``` + +### 3. Event Granularity + +Create specific events rather than generic ones: + +```typescript +// ✅ Good +class UserEmailUpdated { + constructor(public readonly email: string) {} +} + +class UserPasswordChanged { + constructor(public readonly hashedPassword: string) {} +} + +// ❌ Bad +class UserUpdated { + constructor(public readonly updates: Partial) {} +} +``` + +### 4. Error Handling + +Handle errors gracefully within handlers: + +```typescript +private handleLogin = async (event: LoginRequested, emit: (state: AuthState) => void) => { + emit({ ...this.state, isLoading: true, error: null }); + + try { + const user = await this.api.login(event.credentials); + emit({ user, isLoading: false, error: null }); + } catch (error) { + emit({ + user: null, + isLoading: false, + error: error.message + }); + } +}; +``` + +## Summary + +Blocs provide powerful event-driven state management: + +- **Structured**: Clear separation of events and handlers +- **Traceable**: Every state change has an associated event +- **Testable**: Easy to test with event-based assertions +- **Scalable**: Handles complex business logic elegantly + +Choose Blocs when you need structure, traceability, and scalability. For simpler cases, [Cubits](/concepts/cubits) might be more appropriate. diff --git a/apps/docs/concepts/cubits.md b/apps/docs/concepts/cubits.md new file mode 100644 index 00000000..e11a01b8 --- /dev/null +++ b/apps/docs/concepts/cubits.md @@ -0,0 +1,609 @@ +# Cubits + +Cubits are the simplest form of state containers in BlaC. They provide a straightforward way to manage state with direct method calls, making them perfect for most use cases. + +## What is a Cubit? + +A Cubit (Cube + Bit) is a class that: + +- Extends `Cubit` from `@blac/core` +- Holds a single piece of state of type `T` +- Provides methods to update that state +- Notifies listeners when state changes + +Think of a Cubit as a "smart" variable that knows how to update itself and tell others when it changes. + +## Creating a Cubit + +### Basic Cubit + +```typescript +import { Cubit } from '@blac/core'; + +// State can be an object with a primitive value +class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); // Initial state + } + + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); +} + +// Or an object +interface UserState { + name: string; + email: string; + preferences: UserPreferences; +} + +class UserCubit extends Cubit { + constructor(initialUser: UserState) { + super(initialUser); + } + + updateName = (name: string) => { + this.patch({ name }); + }; + + updateEmail = (email: string) => { + this.patch({ email }); + }; +} +``` + +### Key Rules + +1. **Always use arrow functions** for methods that access `this`: + +```typescript +// ✅ Correct +increment = () => this.emit(this.state + 1); + +// ❌ Wrong - loses 'this' context when called from React +increment() { + this.emit(this.state + 1); +} +``` + +2. **Call `super()` with initial state**: + +```typescript +constructor() { + super(initialState); // Required! +} +``` + +## State Updates + +Cubits provide two methods for updating state: + +### emit() + +Replaces the entire state with a new value: + +```typescript +class ThemeCubit extends Cubit<{ theme: 'light' | 'dark' }> { + constructor() { + super({ theme: 'light' }); + } + + toggleTheme = () => { + this.emit({ theme: this.state.theme === 'light' ? 'dark' : 'light' }); + }; + + setTheme = (theme: 'light' | 'dark') => { + this.emit({ theme }); + }; +} +``` + +### patch() + +Updates specific properties of an object state (partial update): + +```typescript +interface FormState { + name: string; + email: string; + phone: string; + address: string; +} + +class FormCubit extends Cubit { + constructor() { + super({ + name: '', + email: '', + phone: '', + address: '', + }); + } + + // Update single field + updateField = (field: keyof FormState, value: string) => { + this.patch({ [field]: value }); + }; + + // Update multiple fields + updateContact = (email: string, phone: string) => { + this.patch({ email, phone }); + }; + + // Reset form + reset = () => { + this.emit({ + name: '', + email: '', + phone: '', + address: '', + }); + }; +} +``` + +## Advanced Patterns + +### Async Operations + +Handle asynchronous operations with proper state management: + +```typescript +interface DataState { + data: T | null; + isLoading: boolean; + error: string | null; +} + +class DataCubit extends Cubit> { + constructor() { + super({ + data: null, + isLoading: false, + error: null, + }); + } + + fetch = async (fetcher: () => Promise) => { + this.emit({ data: null, isLoading: true, error: null }); + + try { + const data = await fetcher(); + this.patch({ data, isLoading: false }); + } catch (error) { + this.patch({ + error: error instanceof Error ? error.message : 'Unknown error', + isLoading: false, + }); + } + }; + + retry = () => { + if (this.lastFetcher) { + this.fetch(this.lastFetcher); + } + }; + + private lastFetcher?: () => Promise; +} +``` + +### Computed Properties + +Use getters for derived state: + +```typescript +interface CartState { + items: CartItem[]; + taxRate: number; + discount: number; +} + +interface CartItem { + id: string; + name: string; + price: number; + quantity: number; +} + +class CartCubit extends Cubit { + constructor() { + super({ + items: [], + taxRate: 0.08, + discount: 0, + }); + } + + // Computed properties + get subtotal() { + return this.state.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + } + + get tax() { + return this.subtotal * this.state.taxRate; + } + + get total() { + return this.subtotal + this.tax - this.state.discount; + } + + get itemCount() { + return this.state.items.reduce((sum, item) => sum + item.quantity, 0); + } + + // Methods + addItem = (item: CartItem) => { + const existing = this.state.items.find((i) => i.id === item.id); + + if (existing) { + this.updateQuantity(item.id, existing.quantity + item.quantity); + } else { + this.patch({ items: [...this.state.items, item] }); + } + }; + + updateQuantity = (id: string, quantity: number) => { + if (quantity <= 0) { + this.removeItem(id); + return; + } + + this.patch({ + items: this.state.items.map((item) => + item.id === id ? { ...item, quantity } : item, + ), + }); + }; + + removeItem = (id: string) => { + this.patch({ + items: this.state.items.filter((item) => item.id !== id), + }); + }; + + applyDiscount = (discount: number) => { + this.patch({ discount: Math.max(0, discount) }); + }; + + clear = () => { + this.patch({ items: [], discount: 0 }); + }; +} +``` + +### Side Effects + +Manage side effects within the Cubit: + +```typescript +class NotificationCubit extends Cubit { + constructor(private maxNotifications = 5) { + super([]); + } + + add = (notification: Omit) => { + const newNotification: Notification = { + ...notification, + id: Date.now().toString(), + }; + + // Add notification + const updated = [newNotification, ...this.state]; + + // Limit number of notifications + if (updated.length > this.maxNotifications) { + updated.pop(); + } + + this.emit(updated); + + // Auto-dismiss after timeout + if (notification.autoDismiss !== false) { + setTimeout(() => { + this.remove(newNotification.id); + }, notification.duration || 5000); + } + }; + + remove = (id: string) => { + this.emit(this.state.filter((n) => n.id !== id)); + }; + + clear = () => { + this.emit([]); + }; +} +``` + +### State Validation + +Ensure state integrity: + +```typescript +interface RegistrationState { + username: string; + email: string; + password: string; + confirmPassword: string; + errors: Record; +} + +class RegistrationCubit extends Cubit { + constructor() { + super({ + username: '', + email: '', + password: '', + confirmPassword: '', + errors: {}, + }); + } + + updateField = (field: keyof RegistrationState, value: string) => { + // Update field + this.patch({ [field]: value }); + + // Validate after update + this.validateField(field, value); + }; + + private validateField = (field: string, value: string) => { + const errors = { ...this.state.errors }; + + switch (field) { + case 'username': + if (value.length < 3) { + errors.username = 'Username must be at least 3 characters'; + } else { + delete errors.username; + } + break; + + case 'email': + if (!value.includes('@')) { + errors.email = 'Invalid email address'; + } else { + delete errors.email; + } + break; + + case 'password': + if (value.length < 8) { + errors.password = 'Password must be at least 8 characters'; + } else { + delete errors.password; + } + // Check confirm password match + if ( + this.state.confirmPassword && + value !== this.state.confirmPassword + ) { + errors.confirmPassword = 'Passwords do not match'; + } else { + delete errors.confirmPassword; + } + break; + + case 'confirmPassword': + if (value !== this.state.password) { + errors.confirmPassword = 'Passwords do not match'; + } else { + delete errors.confirmPassword; + } + break; + } + + this.patch({ errors }); + }; + + get isValid() { + return ( + Object.keys(this.state.errors).length === 0 && + this.state.username && + this.state.email && + this.state.password && + this.state.confirmPassword + ); + } + + submit = async () => { + if (!this.isValid) return; + + // Proceed with registration... + }; +} +``` + +## Lifecycle & Cleanup + +Cubits can perform cleanup when disposed: + +```typescript +class WebSocketCubit extends Cubit { + private ws?: WebSocket; + + constructor(private url: string) { + super({ status: 'disconnected' }); + } + + connect = () => { + this.emit({ status: 'connecting' }); + + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + this.emit({ status: 'connected' }); + }; + + this.ws.onerror = () => { + this.emit({ status: 'error' }); + }; + + this.ws.onclose = () => { + this.emit({ status: 'disconnected' }); + }; + }; + + disconnect = () => { + this.ws?.close(); + }; + + // Called when the last consumer unsubscribes + onDispose = () => { + this.disconnect(); + }; +} +``` + +## Testing Cubits + +Cubits are easy to test: + +```typescript +describe('CounterCubit', () => { + let cubit: CounterCubit; + + beforeEach(() => { + cubit = new CounterCubit(); + }); + + it('should start with initial state', () => { + expect(cubit.state).toEqual({ count: 0 }); + }); + + it('should increment', () => { + cubit.increment(); + expect(cubit.state).toEqual({ count: 1 }); + + cubit.increment(); + expect(cubit.state).toEqual({ count: 2 }); + }); + + it('should notify listeners on state change', () => { + const listener = jest.fn(); + const unsubscribe = cubit.subscribe(listener); + + cubit.increment(); + + expect(listener).toHaveBeenCalledWith({ count: 1 }); + + // Clean up + unsubscribe(); + }); +}); + +// Testing async operations +describe('DataCubit', () => { + it('should handle successful fetch', async () => { + const cubit = new DataCubit(); + const mockUser = { id: '1', name: 'Test' }; + + await cubit.fetch(async () => mockUser); + + expect(cubit.state).toEqual({ + data: mockUser, + isLoading: false, + error: null, + }); + }); + + it('should handle fetch error', async () => { + const cubit = new DataCubit(); + + await cubit.fetch(async () => { + throw new Error('Network error'); + }); + + expect(cubit.state).toEqual({ + data: null, + isLoading: false, + error: 'Network error', + }); + }); +}); +``` + +## Best Practices + +### 1. Single Responsibility + +Each Cubit should manage one feature or domain: + +```typescript +// ✅ Good +class UserProfileCubit extends Cubit {} +class UserSettingsCubit extends Cubit {} + +// ❌ Bad +class UserCubit extends Cubit<{ + profile: UserProfile; + settings: UserSettings; +}> {} +``` + +### 2. Immutable Updates + +Always create new objects/arrays: + +```typescript +// ✅ Good +this.patch({ items: [...this.state.items, newItem] }); + +// ❌ Bad +this.state.items.push(newItem); // Mutation! +this.emit(this.state); +``` + +### 3. Meaningful Method Names + +Use clear, action-oriented names: + +```typescript +// ✅ Good +class AuthCubit { + signIn = () => {}; + signOut = () => {}; + refreshToken = () => {}; +} + +// ❌ Bad +class AuthCubit { + update = () => {}; + change = () => {}; + doThing = () => {}; +} +``` + +### 4. Handle All States + +Consider loading, error, and success states: + +```typescript +// ✅ Good +type State = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: Data } + | { status: 'error'; error: string }; + +// ❌ Incomplete +interface State { + data?: Data; + // What about loading? Errors? +} +``` + +## Summary + +Cubits provide a simple yet powerful way to manage state in BlaC: + +- **Simple API**: Just `emit()` and `patch()` +- **Type Safe**: Full TypeScript support +- **Testable**: Easy to unit test +- **Flexible**: From counters to complex forms + +For more complex scenarios with event-driven architecture, check out [Blocs](/concepts/blocs). diff --git a/apps/docs/concepts/instance-management.md b/apps/docs/concepts/instance-management.md new file mode 100644 index 00000000..49756c78 --- /dev/null +++ b/apps/docs/concepts/instance-management.md @@ -0,0 +1,534 @@ +# Instance Management + +BlaC automatically manages the lifecycle of your Cubits and Blocs, handling creation, sharing, and disposal. This guide explains how BlaC's intelligent instance management works behind the scenes. + +## Automatic Instance Creation + +When you use `useBloc`, BlaC automatically creates instances as needed: + +```typescript +function MyComponent() { + // BlaC creates a CounterCubit instance if it doesn't exist + const [count, counter] = useBloc(CounterCubit); + + return
{count}
; +} +``` + +No providers, no manual instantiation - BlaC handles it all. + +## Instance Sharing + +By default, all components using the same Cubit/Bloc class share the same instance: + +```typescript +// Component A +function HeaderCounter() { + const [count] = useBloc(CounterCubit); + return Header: {count}; +} + +// Component B +function MainCounter() { + const [count, counter] = useBloc(CounterCubit); + return ( +
+ Main: {count} + +
+ ); +} + +// Both components share the same CounterCubit instance +// Clicking + in MainCounter updates HeaderCounter too +``` + +## Instance Identification + +BlaC identifies instances using: + +1. **Class name** (default) - Used as the instance ID +2. **Custom instance ID** (when provided via `instanceId` option) +3. **Generated ID from props** (when using `staticProps`) + +```typescript +// Default: Uses class name as instance ID +const [state1] = useBloc(UserCubit); // Instance ID: "UserCubit" + +// Custom instance ID: Creates separate instance +const [state2] = useBloc(UserCubit, { instanceId: 'admin-user' }); // Instance ID: "admin-user" + +// Generated from props: Primitive values in staticProps create deterministic IDs +const [state3] = useBloc(UserCubit, { + staticProps: { userId: 123, role: 'admin' }, +}); // Instance ID: "role:admin|userId:123" + +// Different instances, different states +``` + +### How Instance IDs are Generated from Props + +When you provide `staticProps`, BlaC can automatically generate a deterministic instance ID by: + +- Extracting primitive values (string, number, boolean, null, undefined) +- Ignoring complex types (objects, arrays, functions) +- Sorting keys alphabetically +- Creating a formatted string like "key1:value1|key2:value2" + +This is useful for creating unique instances based on props without manually specifying IDs. + +## Static Properties + +BlaC supports several static properties to control instance behavior: + +### `static isolated = true` + +Makes each component get its own instance: + +```typescript +class FormCubit extends Cubit { + static isolated = true; // Each component gets its own instance + + constructor() { + super({ name: '', email: '' }); + } +} + +// Component A +function FormA() { + const [state, form] = useBloc(FormCubit); // Instance A + // ... +} + +// Component B +function FormB() { + const [state, form] = useBloc(FormCubit); // Instance B (different!) + // ... +} +``` + +### `static keepAlive = true` + +Prevents the instance from being disposed when no components use it: + +```typescript +class SessionCubit extends Cubit { + static keepAlive = true; // Never dispose this instance + + constructor() { + super({ user: null, token: null }); + this.loadSession(); + } + + // Stays in memory for entire app lifecycle +} +``` + +### `static plugins = []` + +Attach plugins directly to a specific Bloc/Cubit class: + +```typescript +import { PersistencePlugin } from '@bloc/persistence'; + +class SettingsCubit extends Cubit { + static plugins = [ + new PersistencePlugin({ + key: 'app-settings', + storage: localStorage, + }), + ]; + + constructor() { + super({ theme: 'light', language: 'en' }); + } +} +``` + +## Isolated Instances + +Sometimes you want each component to have its own instance. Use the `static isolated = true` property shown above, or alternatively, use unique instance IDs: + +```typescript +function DynamicForm({ formId }: { formId: string }) { + // Each formId gets its own instance + const [state, form] = useBloc(FormCubit, { instanceId: formId }); + // ... +} +``` + +## Lifecycle Management + +### Creation + +Instances are created on first use: + +```typescript +// First render - creates new instance +function App() { + const [state] = useBloc(AppCubit); // New instance created + return
{state.data}
; +} +``` + +### Reference Counting + +BlaC tracks how many components are using each instance: + +```typescript +function Parent() { + const [showA, setShowA] = useState(true); + const [showB, setShowB] = useState(true); + + return ( + <> + {showA && } {/* Uses CounterCubit */} + {showB && } {/* Also uses CounterCubit */} + + + + ); +} + +// CounterCubit instance: +// - Created when first component mounts +// - Stays alive while either component is mounted +// - Disposed when both components unmount +``` + +### Disposal + +Instances are automatically disposed when no components use them: + +```typescript +class WebSocketCubit extends Cubit { + private ws?: WebSocket; + + constructor() { + super({ status: 'disconnected' }); + this.connect(); + } + + connect = () => { + this.ws = new WebSocket('wss://example.com'); + // ... setup + }; + + // Called automatically when disposed + onDispose = () => { + console.log('Cleaning up WebSocket'); + this.ws?.close(); + }; +} + +// WebSocket closes automatically when last component unmounts +``` + +## Static Props and Dynamic Instances + +Pass static props to customize instance initialization: + +```typescript +interface ChatProps { + roomId: string; + userId: string; +} + +class ChatCubit extends Cubit { + constructor(props?: ChatProps) { + super({ messages: [], connected: false }); + // Access props via constructor parameter + this.roomId = props?.roomId; + this.userId = props?.userId; + + // Optional: Set a custom name for debugging + this._name = `ChatCubit_${props?.roomId}`; + } + + private roomId?: string; + private userId?: string; + + connect = () => { + if (!this.roomId) return; + const socket = io(`/room/${this.roomId}`); + // ... + }; +} + +// Usage +function ChatRoom({ roomId, userId }: { roomId: string; userId: string }) { + const [state, chat] = useBloc(ChatCubit, { + instanceId: `chat-${roomId}`, // Unique instance per room + staticProps: { roomId, userId } + }); + + return
{/* Chat UI */}
; +} +``` + +## Advanced Patterns + +### Factory Pattern + +Create instances with custom logic: + +```typescript +class DataCubit extends Cubit> { + static create(fetcher: () => Promise) { + return class extends DataCubit { + constructor() { + super(); + this.fetch(fetcher); + } + }; + } +} + +// Usage +const UserDataCubit = DataCubit.create(() => api.getUser()); +const PostsDataCubit = DataCubit.create(() => api.getPosts()); + +function UserProfile() { + const [userData] = useBloc(UserDataCubit); + const [postsData] = useBloc(PostsDataCubit); + // ... +} +``` + +### Singleton Pattern + +Ensure only one instance exists globally: + +```typescript +let globalInstance: AppCubit | null = null; + +class AppCubit extends Cubit { + static isolated = false; + static keepAlive = true; + + constructor() { + if (globalInstance) { + return globalInstance; + } + super(initialState); + globalInstance = this; + } +} +``` + +### Scoped Instances + +Create instances scoped to specific parts of your app: + +```typescript +// Workspace-scoped instances +function Workspace({ workspaceId }: { workspaceId: string }) { + // All children share these workspace-specific instances + const [projects] = useBloc(ProjectsCubit, { instanceId: `workspace-${workspaceId}` }); + const [members] = useBloc(MembersCubit, { instanceId: `workspace-${workspaceId}` }); + + return ( +
+ + +
+ ); +} +``` + +## Memory Management + +### Weak References + +BlaC uses WeakRef for consumer tracking to prevent memory leaks: + +```typescript +// Internally, BlaC tracks consumers without preventing garbage collection +class BlocInstance { + private consumers = new Set>(); + + addConsumer(consumer: Consumer) { + this.consumers.add(new WeakRef(consumer)); + } +} +``` + +### Cleanup Best Practices + +Always clean up resources in `onDispose`: + +```typescript +class TimerCubit extends Cubit { + private timer?: NodeJS.Timeout; + + constructor() { + super(0); + this.startTimer(); + } + + startTimer = () => { + this.timer = setInterval(() => { + this.emit(this.state + 1); + }, 1000); + }; + + onDispose = () => { + // Clean up timer to prevent memory leaks + if (this.timer) { + clearInterval(this.timer); + } + }; +} +``` + +### Monitoring Instances + +Debug instance lifecycle: + +```typescript +class DebugCubit extends Cubit { + constructor() { + super(initialState); + console.log(`[${this.constructor.name}] Created`); + } + + onDispose = () => { + console.log(`[${this.constructor.name}] Disposed`); + }; +} + +// Enable BlaC logging +Blac.enableLog = true; +``` + +## React Strict Mode + +BlaC handles React Strict Mode's double-mounting gracefully: + +```typescript +// React Strict Mode calls effects twice in development +function StrictModeComponent() { + const [state] = useBloc(MyCubit); + // BlaC ensures only one instance is created + // despite double-mounting +} +``` + +The disposal system uses atomic state transitions to handle this: + +1. First unmount: Marks for disposal +2. Quick remount: Cancels disposal +3. Final unmount: Actually disposes + +## Common Patterns + +### Global State + +App-wide state that persists: + +```typescript +class ThemeCubit extends Cubit { + static keepAlive = true; // Never dispose + + constructor() { + super(loadThemeFromStorage() || 'light'); + } + + setTheme = (theme: Theme) => { + this.emit(theme); + saveThemeToStorage(theme); + }; +} +``` + +### Feature-Scoped State + +State tied to specific features: + +```typescript +class TodoListCubit extends Cubit { + // Shared across all todo list views + // Disposed when leaving todo feature entirely +} +``` + +### Component-Local State + +State for individual component instances: + +```typescript +class DropdownCubit extends Cubit { + static isolated = true; // Each dropdown is independent + + constructor() { + super({ isOpen: false, selectedItem: null }); + } +} +``` + +## Performance Considerations + +### Instance Reuse + +Reusing instances improves performance: + +```typescript +// ✅ Good: Reuses existing instance +function ProductList() { + const [products] = useBloc(ProductsCubit); + return products.map(p => ); +} + +// ❌ Avoid: Creates new instance each time +function ProductItem({ product }: { product: Product }) { + const [state] = useBloc(ProductCubit, { + instanceId: product.id // New instance per product + }); +} +``` + +### Lazy Initialization + +Instances are created only when needed: + +```typescript +class ExpensiveService { + constructor() { + console.log('Expensive initialization'); + // ... heavy setup + } +} + +class ServiceCubit extends Cubit { + private service?: ExpensiveService; + + // Lazy initialize expensive resources + get expensiveService() { + if (!this.service) { + this.service = new ExpensiveService(); + } + return this.service; + } +} +``` + +## Summary + +BlaC's instance management provides: + +- **Automatic lifecycle**: No manual creation or disposal +- **Smart sharing**: Instances shared by default, isolated when needed +- **Flexible identification**: Use class names, custom instance IDs, or auto-generated IDs from props +- **Static configuration**: Control behavior with `isolated`, `keepAlive`, and `plugins` properties +- **Memory efficiency**: Automatic cleanup and weak references +- **Flexible scoping**: Global, feature, or component-level instances +- **React compatibility**: Handles Strict Mode and concurrent features + +Key options for `useBloc`: + +- `instanceId`: Custom instance identifier +- `staticProps`: Props passed to constructor, can generate instance IDs +- `dependencies`: Re-create instance when dependencies change +- `onMount`/`onUnmount`: Lifecycle callbacks + +This intelligent system lets you focus on your business logic while BlaC handles the infrastructure. diff --git a/apps/docs/concepts/state-management.md b/apps/docs/concepts/state-management.md new file mode 100644 index 00000000..7cc90030 --- /dev/null +++ b/apps/docs/concepts/state-management.md @@ -0,0 +1,447 @@ +# State Management + +## Why State Management is the Heart of Every Application + +Let's be honest: state management is one of the hardest problems in software engineering. It's not just about storing values—it's about orchestrating the complex dance between user interactions, business logic, data persistence, and UI updates. Get it wrong, and your application becomes a tangled mess of bugs, performance issues, and unmaintainable code. + +Every successful application eventually faces the same fundamental questions: + +- Where does this piece of state live? +- Who can modify it? +- How do changes propagate through the system? +- How do we test state transitions? +- How do we debug when things go wrong? + +These questions become exponentially harder as applications grow. What starts as a simple `useState` call evolves into a complex web of interdependencies that can bring even experienced teams to their knees. + +## The Real Cost of Poor State Management + +### 1. **Bugs That Multiply** + +When state management is ad-hoc, bugs don't just appear—they multiply. A simple state update in one component can have cascading effects throughout your application. Race conditions emerge. Data gets out of sync. Users see stale information. And the worst part? These bugs are often intermittent and nearly impossible to reproduce consistently. + +### 2. **Development Velocity Grinds to a Halt** + +As your codebase grows, adding new features becomes increasingly dangerous. Developers spend more time understanding existing state interactions than building new functionality. Simple changes require touching multiple files, and every modification carries the risk of breaking something elsewhere. + +### 3. **Testing Becomes a Nightmare** + +When business logic is intertwined with UI components, testing becomes nearly impossible. You can't test a calculation without rendering components. You can't verify state transitions without mocking entire component trees. Your test suite becomes brittle, slow, and provides little confidence. + +### 4. **Performance Degrades Invisibly** + +Poor state management leads to unnecessary re-renders, memory leaks, and sluggish UIs. But these issues creep in slowly. What feels fast with 10 items becomes unusable with 1,000. By the time you notice, refactoring requires a complete rewrite. + +## The Fundamental Problem: Mixing Concerns + +Let's look at how state management typically evolves in a React application: + +### Stage 1: The Honeymoon Phase + +```tsx +function TodoItem() { + const [isComplete, setIsComplete] = useState(false); + + return ( +
+ setIsComplete(e.target.checked)} + /> + Buy milk +
+ ); +} +``` + +This looks clean! State is colocated with the component that uses it. But then requirements change... + +### Stage 2: Growing Complexity + +```tsx +function TodoList() { + const [todos, setTodos] = useState([]); + const [filter, setFilter] = useState('all'); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [lastSync, setLastSync] = useState(null); + + const addTodo = async (text) => { + setIsLoading(true); + setError(null); + + try { + // Optimistic update + const tempId = Date.now(); + const newTodo = { id: tempId, text, completed: false, userId: user?.id }; + setTodos([...todos, newTodo]); + + // Analytics + trackEvent('todo_added', { userId: user?.id }); + + // API call + const savedTodo = await api.createTodo(newTodo); + + // Replace temp with real + setTodos(todos.map((t) => (t.id === tempId ? savedTodo : t))); + setLastSync(new Date()); + } catch (err) { + // Rollback + setTodos(todos.filter((t) => t.id !== tempId)); + setError(err.message); + + // Retry logic + if (err.code === 'NETWORK_ERROR') { + queueForRetry(newTodo); + } + } finally { + setIsLoading(false); + } + }; + + const toggleTodo = async (id) => { + // Similar complexity... + }; + + const deleteTodo = async (id) => { + // And more complexity... + }; + + // Component has become a 500+ line monster mixing: + // - UI rendering + // - Business logic + // - API calls + // - Error handling + // - Analytics + // - Performance optimizations + // - State synchronization +} +``` + +### Stage 3: The Breaking Point + +Your component now has multiple responsibilities: + +- **Presentation**: Rendering UI elements +- **State Management**: Tracking multiple pieces of state +- **Business Logic**: Validation, calculations, transformations +- **Side Effects**: API calls, analytics, logging +- **Error Handling**: Retry logic, rollback mechanisms +- **Performance**: Memoization, debouncing, virtualization + +This violates every principle of good software design. It's untestable, unreusable, and unmaintainable. + +## How BlaC Solves the Root Problem + +BlaC isn't just another state management library—it's an architectural pattern that enforces proper separation of concerns and clean code principles. + +### 1. **Separation of Concerns Through Layered Architecture** + +Inspired by the BLoC pattern, BlaC enforces a clear separation between layers: + +```typescript +// Business Logic Layer - Pure, testable, reusable +class TodoCubit extends Cubit { + constructor( + private todoRepository: TodoRepository, + private analytics: AnalyticsService, + private errorReporter: ErrorReporter + ) { + super({ + todos: [], + filter: 'all', + isLoading: false, + error: null + }); + } + + addTodo = async (text: string) => { + this.patch({ isLoading: true, error: null }); + + try { + const todo = await this.todoRepository.create({ text }); + this.patch({ + todos: [...this.state.todos, todo], + isLoading: false + }); + + this.analytics.track('todo_added'); + } catch (error) { + this.errorReporter.log(error); + this.patch({ + error: error.message, + isLoading: false + }); + } + }; +} + +// Presentation Layer - Simple, focused, declarative +function TodoList() { + const [state, cubit] = useBloc(TodoCubit); + + return ( +
+ {state.isLoading && } + {state.error && } + {state.todos.map(todo => ( + + ))} +
+ ); +} +``` + +### 2. **Dependency Injection for Testability** + +BlaC encourages dependency injection, making your business logic completely testable: + +```typescript +// Easy to test with mock dependencies +describe('TodoCubit', () => { + it('should add todo successfully', async () => { + const mockRepo = { + create: jest.fn().mockResolvedValue({ id: 1, text: 'Test' }), + }; + const mockAnalytics = { track: jest.fn() }; + + const cubit = new TodoCubit(mockRepo, mockAnalytics, mockErrorReporter); + await cubit.addTodo('Test'); + + expect(cubit.state.todos).toHaveLength(1); + expect(mockAnalytics.track).toHaveBeenCalledWith('todo_added'); + }); +}); +``` + +### 3. **Event-Driven Architecture for Complex Flows** + +For complex scenarios, BlaC's Bloc pattern provides event-driven state management: + +```typescript +// Events represent user intentions +class UserAuthenticated { + constructor(public readonly user: User) {} +} + +class DataRefreshRequested {} + +class NetworkStatusChanged { + constructor(public readonly isOnline: boolean) {} +} + +// Bloc orchestrates complex state transitions +class AppBloc extends Bloc { + constructor(dependencies: AppDependencies) { + super(initialState); + + // Clear event flow + this.on(UserAuthenticated, this.handleUserAuthenticated); + this.on(DataRefreshRequested, this.handleDataRefresh); + this.on(NetworkStatusChanged, this.handleNetworkChange); + } + + private handleUserAuthenticated = async ( + event: UserAuthenticated, + emit: (state: AppState) => void, + ) => { + // Complex orchestration logic + emit({ ...this.state, user: event.user, isAuthenticated: true }); + + // Trigger cascading events + this.add(new DataRefreshRequested()); + this.add(new UserPreferencesLoaded()); + this.add(new NotificationServiceInitialized()); + }; +} +``` + +### 4. **Instance Management That Mirrors Your Mental Model** + +BlaC provides flexible instance management that matches how you think about state: + +```typescript +// Global state - shared across the app +class AuthCubit extends Cubit { + static keepAlive = true; // Persists even without consumers +} + +// Feature state - shared within a feature +class ShoppingCartCubit extends Cubit { + // Default behavior - shared instance per class +} + +// Component state - isolated per component +class FormCubit extends Cubit { + static isolated = true; // Each form gets its own instance +} + +// Keyed state - multiple named instances +const [state, cubit] = useBloc(ChatCubit, { + instanceId: `chat-${roomId}`, // Separate instance per chat room +}); +``` + +## The Architectural Benefits + +### 1. **Clear Mental Model** + +With BlaC, you always know where to look: + +- **Business Logic**: In Cubits/Blocs +- **UI Logic**: In Components +- **Data Access**: In Repositories +- **Side Effects**: In Services + +### 2. **Predictable Data Flow** + +State changes follow a unidirectional flow: + +``` +User Action → Cubit/Bloc Method → State Update → UI Re-render +``` + +This makes debugging straightforward—you can trace any state change back to its origin. + +### 3. **Incremental Adoption** + +You don't need to rewrite your entire app. Start with one feature: + +```typescript +// Start small +class SettingsCubit extends Cubit { + toggleTheme = () => { + this.patch({ darkMode: !this.state.darkMode }); + }; +} + +// Gradually expand +class AppCubit extends Cubit { + constructor() { + super(computeInitialState()); + + // Compose state from multiple sources + this.subscribeToSubstates(); + } +} +``` + +### 4. **Performance by Default** + +BlaC's proxy-based dependency tracking means components only re-render when the specific data they use changes: + +```typescript +function UserAvatar() { + const [state] = useBloc(UserCubit); + // Only re-renders when state.user.avatarUrl changes + return ; +} + +function UserStats() { + const [state] = useBloc(UserCubit); + // Only re-renders when state.stats changes + return
{state.stats.postsCount} posts
; +} +``` + +## Real-World Patterns + +### Repository Pattern for Data Access + +```typescript +interface TodoRepository { + getAll(): Promise; + create(data: CreateTodoDto): Promise; + update(id: string, data: UpdateTodoDto): Promise; + delete(id: string): Promise; +} + +class TodoCubit extends Cubit { + constructor(private repository: TodoRepository) { + super(initialState); + } + + // Clean separation of concerns + loadTodos = async () => { + this.patch({ isLoading: true }); + try { + const todos = await this.repository.getAll(); + this.patch({ todos, isLoading: false }); + } catch (error) { + this.patch({ error: error.message, isLoading: false }); + } + }; +} +``` + +### Service Layer for Business Operations + +```typescript +class CheckoutService { + constructor( + private payment: PaymentGateway, + private inventory: InventoryService, + private shipping: ShippingService, + ) {} + + async processOrder(cart: Cart): Promise { + // Complex business logic orchestration + await this.inventory.reserve(cart.items); + const payment = await this.payment.charge(cart.total); + const shipping = await this.shipping.schedule(cart); + + return new Order({ cart, payment, shipping }); + } +} + +class CheckoutCubit extends Cubit { + constructor(private checkout: CheckoutService) { + super(initialState); + } + + processCheckout = async () => { + this.emit({ status: 'processing' }); + + try { + const order = await this.checkout.processOrder(this.state.cart); + this.emit({ status: 'completed', order }); + } catch (error) { + this.emit({ status: 'failed', error: error.message }); + } + }; +} +``` + +## The Bottom Line + +State management is hard because it touches every aspect of your application. It's not just about storing values—it's about: + +- **Architecture**: How you structure your code +- **Testability**: How you verify behavior +- **Maintainability**: How you evolve your application +- **Performance**: How you scale to thousands of users +- **Developer Experience**: How quickly new team members can contribute + +BlaC addresses all these concerns by providing: + +1. **Clear architectural patterns** that separate business logic from UI +2. **Dependency injection** that makes testing trivial +3. **Event-driven architecture** for complex state flows +4. **Flexible instance management** that matches your mental model +5. **Automatic performance optimization** without manual work +6. **TypeScript-first design** with zero boilerplate + +Most importantly, BlaC helps you write code that is easy to understand, easy to test, and easy to change. Because at the end of the day, the best state management solution is the one that gets out of your way and lets you focus on building great applications. + +## Getting Started + +Ready to bring structure to your state management? Start with: + +1. [Cubits](/concepts/cubits) - Simple, direct state updates +2. [Blocs](/concepts/blocs) - Event-driven state management +3. [Instance Management](/concepts/instance-management) - Control state lifecycle +4. [Testing Patterns](/patterns/testing) - Write bulletproof tests + +Remember: good architecture isn't about following rules—it's about making your code easier to understand, test, and change. BlaC gives you the tools; how you use them is up to you. diff --git a/apps/docs/docs/api/core-classes.md b/apps/docs/docs/api/core-classes.md index 08fcf38e..f4a55f69 100644 --- a/apps/docs/docs/api/core-classes.md +++ b/apps/docs/docs/api/core-classes.md @@ -13,23 +13,23 @@ Blac provides three primary classes for state management: ### Properties -| Name | Type | Description | -|------|------|-------------| -| `state` | `S` | The current state of the container | -| `props` | `P \| null` | Props passed during Bloc instance creation (can be null) | -| `lastUpdate` | `number` | Timestamp when the state was last updated | +| Name | Type | Description | +| ------------ | ----------- | -------------------------------------------------------- | +| `state` | `S` | The current state of the container | +| `props` | `P \| null` | Props passed during Bloc instance creation (can be null) | +| `lastUpdate` | `number` | Timestamp when the state was last updated | ### Static Properties -| Name | Type | Default | Description | -|------|------|---------|-------------| -| `isolated` | `boolean` | `false` | When true, every consumer will receive its own unique instance | +| Name | Type | Default | Description | +| ----------- | --------- | ------- | --------------------------------------------------------------------- | +| `isolated` | `boolean` | `false` | When true, every consumer will receive its own unique instance | | `keepAlive` | `boolean` | `false` | When true, the instance persists even when no components are using it | ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| +| Name | Parameters | Return Type | Description | +| ---- | -------------------------------------------------------------------- | ------------ | --------------------------------------------------------------- | | `on` | `event: BlacEvent, listener: StateListener, signal?: AbortSignal` | `() => void` | Subscribes to state changes and returns an unsubscribe function | ## Cubit<S, P> @@ -49,10 +49,10 @@ constructor(initialState: S) ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `emit` | `state: S` | `void` | Replaces the entire state | -| `patch` | `statePatch: S extends object ? Partial : S, ignoreChangeCheck?: boolean` | `void` | Updates specific properties of the state | +| Name | Parameters | Return Type | Description | +| ------- | ---------------------------------------------------------------------------- | ----------- | ---------------------------------------- | +| `emit` | `state: S` | `void` | Replaces the entire state | +| `patch` | `statePatch: S extends object ? Partial : S, ignoreChangeCheck?: boolean` | `void` | Updates specific properties of the state | ### Example @@ -77,14 +77,14 @@ class CounterCubit extends Cubit<{ count: number }> { } ``` -## Bloc<S, A, P> +## Bloc<S, E, P> -`Bloc` is a more sophisticated state container that follows the reducer pattern. It extends `BlocBase` and adds action handling. +`Bloc` is a more sophisticated state container that uses an event-handler pattern. It extends `BlocBase` and manages state transitions by registering handlers for specific event classes. ### Type Parameters - `S` - The state type -- `A` - The action type +- `E` - The base type or union of event classes that this Bloc can process. - `P` - The props type (optional) ### Constructor @@ -95,43 +95,46 @@ constructor(initialState: S) ### Methods -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `add` | `action: A` | `void` | Dispatches an action to the reducer | -| `reducer` | `action: A, state: S` | `S` | Determines how state changes in response to actions (must be implemented) | +| Name | Parameters | Return Type | Description | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | +| `on` | `eventConstructor: new (...args: any[]) => E, handler: (event: InstanceType, emit: (newState: S) => void) => void` | `void` | Registers an event handler for a specific event class. | +| `add` | `event: E` | `void` | Dispatches an event instance to its registered handler. The handler is looked up based on the event's constructor. | ### Example ```tsx -// Define actions -type CounterAction = - | { type: 'increment' } - | { type: 'decrement' } - | { type: 'reset' }; +// Define event classes +class IncrementEvent { + constructor(public readonly value: number = 1) {} +} +class DecrementEvent {} +class ResetEvent {} + +// Union type for all possible event classes (optional but can be useful) +type CounterEvent = IncrementEvent | DecrementEvent | ResetEvent; -class CounterBloc extends Bloc<{ count: number }, CounterAction> { +class CounterBloc extends Bloc<{ count: number }, CounterEvent> { constructor() { super({ count: 0 }); - } - // Implement the reducer method - reducer = (action: CounterAction, state: { count: number }) => { - switch (action.type) { - case 'increment': - return { count: state.count + 1 }; - case 'decrement': - return { count: state.count - 1 }; - case 'reset': - return { count: 0 }; - default: - return state; - } - }; + // Register event handlers + this.on(IncrementEvent, (event, emit) => { + emit({ count: this.state.count + event.value }); + }); + + this.on(DecrementEvent, (_event, emit) => { + emit({ count: this.state.count - 1 }); + }); + + this.on(ResetEvent, (_event, emit) => { + emit({ count: 0 }); + }); + } - // Helper methods to dispatch actions - increment = () => this.add({ type: 'increment' }); - decrement = () => this.add({ type: 'decrement' }); - reset = () => this.add({ type: 'reset' }); + // Helper methods to dispatch events (optional but common) + increment = (value?: number) => this.add(new IncrementEvent(value)); + decrement = () => this.add(new DecrementEvent()); + reset = () => this.add(new ResetEvent()); } ``` @@ -139,13 +142,13 @@ class CounterBloc extends Bloc<{ count: number }, CounterAction> { `BlacEvent` is an enum that defines the different events that can be dispatched by Blac. -| Event | Description | -|-------|-------------| -| `StateChange` | Triggered when a state changes | -| `Error` | Triggered when an error occurs | -| `Action` | Triggered when an action is dispatched | +| Event | Description | +| ------------- | ------------------------------------------------------ | +| `StateChange` | Triggered when a state changes | +| `Error` | Triggered when an error occurs | +| `Action` | Triggered when an event is dispatched via `bloc.add()` | ## Choosing Between Cubit and Bloc -- Use **Cubit** when you have simple state logic and don't need the reducer pattern -- Use **Bloc** when you have complex state transitions, want to leverage the reducer pattern, or need a more formal action-based approach \ No newline at end of file +- Use **Cubit** for simpler state logic where direct state emission (`emit`, `patch`) is sufficient. +- Use **Bloc** for more complex state logic, when you want to process distinct event types with dedicated handlers, or when you need a more formal event-driven approach to manage state transitions. diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs new file mode 100644 index 00000000..d0bbadce --- /dev/null +++ b/apps/docs/eslint.config.mjs @@ -0,0 +1,38 @@ +import js from '@eslint/js'; +import reactHooks from 'eslint-plugin-react-hooks'; +import tseslint from 'typescript-eslint'; + +export default [ + { + ignores: [ + '.vitepress/**', + 'node_modules/**', + 'dist/**', + 'build/**', + '.turbo/**', + 'coverage/**', + 'out/**', + 'output/**', + '.cache/**', + 'temp/**', + 'tmp/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { 'react-hooks': reactHooks }, + rules: { + 'react-hooks/exhaustive-deps': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + args: 'after-used', + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'no-undef': 'off', + }, + }, +]; diff --git a/apps/docs/examples/authentication.md b/apps/docs/examples/authentication.md new file mode 100644 index 00000000..507dd9d2 --- /dev/null +++ b/apps/docs/examples/authentication.md @@ -0,0 +1,920 @@ +# Authentication Example + +A complete authentication system demonstrating user login, registration, session management, and protected routes using BlaC. + +## Basic Authentication Cubit + +Let's start with a simple authentication implementation: + +### State Definition + +```typescript +interface User { + id: string; + email: string; + name: string; + role: 'user' | 'admin'; + avatarUrl?: string; +} + +interface AuthState { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; + sessionExpiry: Date | null; +} +``` + +### Authentication Cubit + +```typescript +import { Cubit } from '@blac/core'; + +export class AuthCubit extends Cubit { + constructor(private authAPI: AuthAPI) { + super({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + sessionExpiry: null, + }); + + // Check for existing session on init + this.checkSession(); + } + + // Session management + checkSession = async () => { + const token = localStorage.getItem('auth_token'); + if (!token) return; + + this.patch({ isLoading: true }); + + try { + const user = await this.authAPI.validateToken(token); + this.patch({ + user, + isAuthenticated: true, + isLoading: false, + sessionExpiry: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours + }); + } catch (error) { + // Invalid token, clear it + localStorage.removeItem('auth_token'); + this.patch({ + user: null, + isAuthenticated: false, + isLoading: false, + }); + } + }; + + // Login + login = async (email: string, password: string) => { + this.patch({ isLoading: true, error: null }); + + try { + const { user, token, expiresIn } = await this.authAPI.login( + email, + password, + ); + + // Store token + localStorage.setItem('auth_token', token); + + // Update state + this.patch({ + user, + isAuthenticated: true, + isLoading: false, + sessionExpiry: new Date(Date.now() + expiresIn * 1000), + }); + + return { success: true }; + } catch (error) { + this.patch({ + isLoading: false, + error: error.message || 'Login failed', + }); + return { success: false, error: error.message }; + } + }; + + // Logout + logout = async () => { + this.patch({ isLoading: true }); + + try { + await this.authAPI.logout(); + } catch (error) { + // Log error but continue with local logout + console.error('Logout API error:', error); + } + + // Clear local state regardless + localStorage.removeItem('auth_token'); + this.emit({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + sessionExpiry: null, + }); + }; + + // Registration + register = async (email: string, password: string, name: string) => { + this.patch({ isLoading: true, error: null }); + + try { + const { user, token, expiresIn } = await this.authAPI.register({ + email, + password, + name, + }); + + // Store token + localStorage.setItem('auth_token', token); + + // Update state + this.patch({ + user, + isAuthenticated: true, + isLoading: false, + sessionExpiry: new Date(Date.now() + expiresIn * 1000), + }); + + return { success: true }; + } catch (error) { + this.patch({ + isLoading: false, + error: error.message || 'Registration failed', + }); + return { success: false, error: error.message }; + } + }; + + // Update user profile + updateProfile = async (updates: Partial) => { + if (!this.state.user) return; + + // Optimistic update + const previousUser = this.state.user; + this.patch({ + user: { ...this.state.user, ...updates }, + }); + + try { + const updatedUser = await this.authAPI.updateProfile(updates); + this.patch({ user: updatedUser }); + } catch (error) { + // Revert on error + this.patch({ + user: previousUser, + error: 'Failed to update profile', + }); + } + }; + + // Clear error + clearError = () => { + this.patch({ error: null }); + }; + + // Computed properties + get isSessionValid() { + return this.state.sessionExpiry && this.state.sessionExpiry > new Date(); + } + + get sessionRemainingMinutes() { + if (!this.state.sessionExpiry) return 0; + const remaining = this.state.sessionExpiry.getTime() - Date.now(); + return Math.max(0, Math.floor(remaining / 60000)); + } +} +``` + +## React Components + +### Login Form + +```tsx +import { useBloc } from '@blac/react'; +import { AuthCubit } from './AuthCubit'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export function LoginForm() { + const [state, auth] = useBloc(AuthCubit); + const navigate = useNavigate(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await auth.login(email, password); + + if (result.success) { + navigate('/dashboard'); + } + }; + + return ( +
+

Login

+ + {state.error && ( +
+ {state.error} + +
+ )} + +
+ + setEmail(e.target.value)} + required + disabled={state.isLoading} + /> +
+ +
+ + setPassword(e.target.value)} + required + disabled={state.isLoading} + /> +
+ + + +

+ Don't have an account? Register +

+
+ ); +} +``` + +### Protected Route Component + +```tsx +import { useBloc } from '@blac/react'; +import { AuthCubit } from './AuthCubit'; +import { Navigate, useLocation } from 'react-router-dom'; + +interface ProtectedRouteProps { + children: React.ReactNode; + requiredRole?: 'user' | 'admin'; +} + +export function ProtectedRoute({ + children, + requiredRole, +}: ProtectedRouteProps) { + const [state] = useBloc(AuthCubit); + const location = useLocation(); + + // Still loading initial session + if (state.isLoading && !state.user) { + return
Loading...
; + } + + // Not authenticated + if (!state.isAuthenticated) { + return ; + } + + // Role check + if (requiredRole && state.user?.role !== requiredRole) { + return ; + } + + return <>{children}; +} + +// Usage +function App() { + return ( + + } /> + } /> + + + + + } + /> + + + + + } + /> + + ); +} +``` + +### User Profile Component + +```tsx +function UserProfile() { + const [state, auth] = useBloc(AuthCubit); + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ + name: state.user?.name || '', + avatarUrl: state.user?.avatarUrl || '', + }); + + if (!state.user) return null; + + const handleSave = async () => { + await auth.updateProfile(formData); + setIsEditing(false); + }; + + return ( +
+

Profile

+ + {state.error &&
{state.error}
} + + {isEditing ? ( +
{ + e.preventDefault(); + handleSave(); + }} + > + setFormData({ ...formData, name: e.target.value })} + placeholder="Name" + /> + + setFormData({ ...formData, avatarUrl: e.target.value }) + } + placeholder="Avatar URL" + /> + + +
+ ) : ( +
+ Avatar +

{state.user.name}

+

{state.user.email}

+

Role: {state.user.role}

+ +
+ )} + +
+

Session expires in: {auth.sessionRemainingMinutes} minutes

+
+ + +
+ ); +} +``` + +## Advanced: Event-Driven Auth with Bloc + +For more complex authentication flows, use the Bloc pattern: + +### Auth Events + +```typescript +abstract class AuthEvent {} + +class LoginRequested extends AuthEvent { + constructor( + public readonly email: string, + public readonly password: string, + ) { + super(); + } +} + +class LogoutRequested extends AuthEvent {} + +class RegisterRequested extends AuthEvent { + constructor( + public readonly email: string, + public readonly password: string, + public readonly name: string, + ) { + super(); + } +} + +class SessionChecked extends AuthEvent {} + +class TokenRefreshed extends AuthEvent { + constructor(public readonly token: string) { + super(); + } +} + +class SessionExpired extends AuthEvent {} + +class ProfileUpdateRequested extends AuthEvent { + constructor(public readonly updates: Partial) { + super(); + } +} + +type AuthEventType = + | LoginRequested + | LogoutRequested + | RegisterRequested + | SessionChecked + | TokenRefreshed + | SessionExpired + | ProfileUpdateRequested; +``` + +### Auth Bloc + +```typescript +import { Bloc } from '@blac/core'; + +export class AuthBloc extends Bloc { + private refreshTimer?: NodeJS.Timeout; + + constructor(private authAPI: AuthAPI) { + super({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + sessionExpiry: null, + }); + + // Register event handlers + this.on(LoginRequested, this.handleLogin); + this.on(LogoutRequested, this.handleLogout); + this.on(RegisterRequested, this.handleRegister); + this.on(SessionChecked, this.handleSessionCheck); + this.on(TokenRefreshed, this.handleTokenRefresh); + this.on(SessionExpired, this.handleSessionExpired); + this.on(ProfileUpdateRequested, this.handleProfileUpdate); + + // Check session on init + this.add(new SessionChecked()); + } + + private handleLogin = async ( + event: LoginRequested, + emit: (state: AuthState) => void, + ) => { + emit({ ...this.state, isLoading: true, error: null }); + + try { + const response = await this.authAPI.login(event.email, event.password); + + localStorage.setItem('auth_token', response.token); + if (response.refreshToken) { + localStorage.setItem('refresh_token', response.refreshToken); + } + + emit({ + user: response.user, + isAuthenticated: true, + isLoading: false, + error: null, + sessionExpiry: new Date(Date.now() + response.expiresIn * 1000), + }); + + // Set up token refresh + this.scheduleTokenRefresh(response.expiresIn); + } catch (error) { + emit({ + ...this.state, + isLoading: false, + error: error.message || 'Login failed', + }); + } + }; + + private handleLogout = async ( + _: LogoutRequested, + emit: (state: AuthState) => void, + ) => { + // Clear refresh timer + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + try { + await this.authAPI.logout(); + } catch (error) { + console.error('Logout error:', error); + } + + localStorage.removeItem('auth_token'); + localStorage.removeItem('refresh_token'); + + emit({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + sessionExpiry: null, + }); + }; + + private handleTokenRefresh = async ( + _: TokenRefreshed, + emit: (state: AuthState) => void, + ) => { + const refreshToken = localStorage.getItem('refresh_token'); + if (!refreshToken) { + this.add(new SessionExpired()); + return; + } + + try { + const response = await this.authAPI.refreshToken(refreshToken); + + localStorage.setItem('auth_token', response.token); + + emit({ + ...this.state, + sessionExpiry: new Date(Date.now() + response.expiresIn * 1000), + }); + + // Schedule next refresh + this.scheduleTokenRefresh(response.expiresIn); + } catch (error) { + this.add(new SessionExpired()); + } + }; + + private scheduleTokenRefresh(expiresIn: number) { + // Refresh 5 minutes before expiry + const refreshIn = (expiresIn - 300) * 1000; + + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + this.add(new TokenRefreshed(localStorage.getItem('auth_token')!)); + }, refreshIn); + } + + // Public methods + login = (email: string, password: string) => { + this.add(new LoginRequested(email, password)); + }; + + logout = () => this.add(new LogoutRequested()); + + register = (email: string, password: string, name: string) => { + this.add(new RegisterRequested(email, password, name)); + }; + + updateProfile = (updates: Partial) => { + this.add(new ProfileUpdateRequested(updates)); + }; + + dispose() { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + super.dispose(); + } +} +``` + +## Social Authentication + +Extend authentication with social providers: + +```typescript +interface SocialAuthState extends AuthState { + availableProviders: string[]; + isLinkingAccount: boolean; +} + +export class SocialAuthCubit extends Cubit { + constructor(private authAPI: AuthAPI) { + super({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + sessionExpiry: null, + availableProviders: ['google', 'github', 'twitter'], + isLinkingAccount: false, + }); + } + + loginWithProvider = async (provider: string) => { + this.patch({ isLoading: true, error: null }); + + try { + // Redirect to OAuth provider + const authUrl = await this.authAPI.getOAuthUrl(provider); + window.location.href = authUrl; + } catch (error) { + this.patch({ + isLoading: false, + error: `Failed to login with ${provider}`, + }); + } + }; + + handleOAuthCallback = async (code: string, provider: string) => { + this.patch({ isLoading: true, error: null }); + + try { + const response = await this.authAPI.handleOAuthCallback(code, provider); + + localStorage.setItem('auth_token', response.token); + + this.patch({ + user: response.user, + isAuthenticated: true, + isLoading: false, + sessionExpiry: new Date(Date.now() + response.expiresIn * 1000), + }); + + return { success: true }; + } catch (error) { + this.patch({ + isLoading: false, + error: error.message, + }); + return { success: false, error: error.message }; + } + }; + + linkAccount = async (provider: string) => { + if (!this.state.user) return; + + this.patch({ isLinkingAccount: true, error: null }); + + try { + const updatedUser = await this.authAPI.linkSocialAccount( + this.state.user.id, + provider, + ); + + this.patch({ + user: updatedUser, + isLinkingAccount: false, + }); + } catch (error) { + this.patch({ + isLinkingAccount: false, + error: `Failed to link ${provider} account`, + }); + } + }; +} +``` + +### Social Login Component + +```tsx +function SocialLoginButtons() { + const [state, auth] = useBloc(SocialAuthCubit); + + return ( +
+
Or login with
+ + {state.availableProviders.map((provider) => ( + + ))} +
+ ); +} +``` + +## Session Management + +Advanced session handling with activity tracking: + +```typescript +export class SessionCubit extends Cubit { + private activityTimer?: NodeJS.Timeout; + private warningTimer?: NodeJS.Timeout; + + constructor( + private authAPI: AuthAPI, + private options = { + sessionTimeout: 30 * 60 * 1000, // 30 minutes + warningBefore: 5 * 60 * 1000, // 5 minutes + }, + ) { + super({ + lastActivity: new Date(), + showWarning: false, + remainingTime: options.sessionTimeout, + }); + + // Start monitoring + this.startActivityMonitoring(); + } + + private startActivityMonitoring() { + // Listen for user activity + const events = ['mousedown', 'keydown', 'touchstart', 'scroll']; + + const updateActivity = () => { + this.patch({ lastActivity: new Date() }); + this.resetTimers(); + }; + + events.forEach((event) => { + window.addEventListener(event, updateActivity); + }); + + this.resetTimers(); + } + + private resetTimers() { + // Clear existing timers + if (this.activityTimer) clearTimeout(this.activityTimer); + if (this.warningTimer) clearTimeout(this.warningTimer); + + // Set warning timer + this.warningTimer = setTimeout(() => { + this.patch({ showWarning: true }); + }, this.options.sessionTimeout - this.options.warningBefore); + + // Set logout timer + this.activityTimer = setTimeout(() => { + this.handleTimeout(); + }, this.options.sessionTimeout); + } + + private handleTimeout = async () => { + // Emit session expired event or logout + await this.authAPI.logout(); + window.location.href = '/login?reason=timeout'; + }; + + extendSession = () => { + this.patch({ showWarning: false }); + this.resetTimers(); + }; + + get timeRemaining() { + const elapsed = Date.now() - this.state.lastActivity.getTime(); + const remaining = this.options.sessionTimeout - elapsed; + return Math.max(0, remaining); + } +} +``` + +## Testing + +Test authentication flows: + +```typescript +import { AuthCubit } from './AuthCubit'; +import { MockAuthAPI } from './mocks'; + +describe('AuthCubit', () => { + let cubit: AuthCubit; + let mockAPI: MockAuthAPI; + + beforeEach(() => { + mockAPI = new MockAuthAPI(); + cubit = new AuthCubit(mockAPI); + localStorage.clear(); + }); + + describe('login', () => { + it('should login successfully', async () => { + mockAPI.login.mockResolvedValue({ + user: { + id: '1', + email: 'test@example.com', + name: 'Test User', + role: 'user', + }, + token: 'mock-token', + expiresIn: 3600, + }); + + const result = await cubit.login('test@example.com', 'password'); + + expect(result.success).toBe(true); + expect(cubit.state.isAuthenticated).toBe(true); + expect(cubit.state.user?.email).toBe('test@example.com'); + expect(localStorage.getItem('auth_token')).toBe('mock-token'); + }); + + it('should handle login failure', async () => { + mockAPI.login.mockRejectedValue(new Error('Invalid credentials')); + + const result = await cubit.login('test@example.com', 'wrong-password'); + + expect(result.success).toBe(false); + expect(cubit.state.isAuthenticated).toBe(false); + expect(cubit.state.error).toBe('Invalid credentials'); + }); + }); + + describe('session management', () => { + it('should restore session from token', async () => { + localStorage.setItem('auth_token', 'valid-token'); + + mockAPI.validateToken.mockResolvedValue({ + id: '1', + email: 'test@example.com', + name: 'Test User', + role: 'user', + }); + + await cubit.checkSession(); + + expect(cubit.state.isAuthenticated).toBe(true); + expect(cubit.state.user?.email).toBe('test@example.com'); + }); + + it('should clear invalid token', async () => { + localStorage.setItem('auth_token', 'invalid-token'); + mockAPI.validateToken.mockRejectedValue(new Error('Invalid token')); + + await cubit.checkSession(); + + expect(cubit.state.isAuthenticated).toBe(false); + expect(localStorage.getItem('auth_token')).toBeNull(); + }); + }); +}); +``` + +## Key Patterns Demonstrated + +1. **Session Management**: Token storage, validation, and refresh +2. **Protected Routes**: Role-based access control +3. **Error Handling**: User-friendly error messages +4. **Optimistic Updates**: Immediate UI feedback +5. **Social Authentication**: OAuth integration +6. **Session Timeout**: Activity monitoring and warnings +7. **Event-Driven Architecture**: Clear authentication flow with Bloc +8. **Testing**: Comprehensive auth flow testing + +## Security Best Practices + +1. **Never store sensitive data in state**: Only non-sensitive user info +2. **Use HTTPS-only cookies**: For production auth tokens +3. **Implement CSRF protection**: For state-changing operations +4. **Rate limiting**: Prevent brute force attacks +5. **Token rotation**: Regular token refresh +6. **Secure storage**: Use secure storage APIs when available + +## Next Steps + +- [Form Management](/examples/forms) - Complex form validation +- [API Integration](/examples/api-integration) - Advanced API patterns +- [Real-time Features](/examples/real-time) - WebSocket authentication +- [Testing Guide](/guides/testing) - Comprehensive testing strategies diff --git a/apps/docs/examples/counter.md b/apps/docs/examples/counter.md new file mode 100644 index 00000000..8bda9cf8 --- /dev/null +++ b/apps/docs/examples/counter.md @@ -0,0 +1,520 @@ +# Counter Example + +A simple counter demonstrating the basics of BlaC state management. + +## Basic Counter + +The simplest possible example - a number that increments and decrements. + +### Cubit Implementation + +```typescript +import { Cubit } from '@blac/core'; + +export class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); // Initial state + } + + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); + reset = () => this.emit({ count: 0 }); +} +``` + +### React Component + +```tsx +import { useBloc } from '@blac/react'; +import { CounterCubit } from './CounterCubit'; + +function Counter() { + const [state, cubit] = useBloc(CounterCubit); + + return ( +
+

Count: {state.count}

+ + + +
+ ); +} +``` + +## Advanced Counter + +A more complex example with additional features. + +### Enhanced Cubit + +```typescript +interface CounterState { + value: number; + step: number; + min: number; + max: number; + history: number[]; +} + +export class AdvancedCounterCubit extends Cubit { + constructor() { + super({ + value: 0, + step: 1, + min: -10, + max: 10, + history: [0], + }); + } + + increment = () => { + const newValue = Math.min( + this.state.value + this.state.step, + this.state.max, + ); + + this.emit({ + ...this.state, + value: newValue, + history: [...this.state.history, newValue], + }); + }; + + decrement = () => { + const newValue = Math.max( + this.state.value - this.state.step, + this.state.min, + ); + + this.emit({ + ...this.state, + value: newValue, + history: [...this.state.history, newValue], + }); + }; + + setStep = (step: number) => { + this.patch({ step: Math.max(1, step) }); + }; + + setLimits = (min: number, max: number) => { + this.patch({ + min, + max, + value: Math.max(min, Math.min(max, this.state.value)), + }); + }; + + undo = () => { + if (this.state.history.length <= 1) return; + + const newHistory = this.state.history.slice(0, -1); + const previousValue = newHistory[newHistory.length - 1]; + + this.patch({ + value: previousValue, + history: newHistory, + }); + }; + + reset = () => { + this.emit({ + ...this.state, + value: 0, + history: [0], + }); + }; +} +``` + +### Enhanced Component + +```tsx +function AdvancedCounter() { + const [state, cubit] = useBloc(AdvancedCounterCubit); + const canUndo = state.history.length > 1; + const canIncrement = state.value < state.max; + const canDecrement = state.value > state.min; + + return ( +
+

Advanced Counter

+ +
+

{state.value}

+

+ Range: {state.min} to {state.max} +

+
+ +
+ + + + + + + +
+ +
+ + + + + +
+ +
+

History ({state.history.length} values)

+

{state.history.join(' → ')}

+
+
+ ); +} +``` + +## Multiple Counters + +Demonstrating instance management with multiple independent counters. + +### Isolated Counter + +```typescript +class IsolatedCounterCubit extends Cubit<{ count: number }> { + static isolated = true; // Each component gets its own instance + + constructor() { + super({ count: 0 }); + } + + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); +} + +function MultipleCounters() { + return ( +
+

Independent Counters

+ + + +
+ ); +} + +function CounterWidget({ title }: { title: string }) { + const [state, cubit] = useBloc(IsolatedCounterCubit); + + return ( +
+

{title}: {state.count}

+ + +
+ ); +} +``` + +### Named Instances + +```typescript +function NamedCounters() { + const [stateA] = useBloc(CounterCubit, { instanceId: 'counter-a' }); + const [stateB] = useBloc(CounterCubit, { instanceId: 'counter-b' }); + const [stateC] = useBloc(CounterCubit, { instanceId: 'counter-c' }); + + return ( +
+

Named Counter Instances

+ + + + +
+

Current Scores: {stateA.count} - {stateB.count} (Round {stateC.count})

+
+
+ ); +} + +function NamedCounter({ instanceId, label }: { instanceId: string; label: string }) { + const [state, cubit] = useBloc(CounterCubit, { instanceId }); + + return ( +
+

{label}: {state.count}

+ + +
+ ); +} +``` + +## Event-Driven Counter (Bloc) + +Using Bloc for event-driven state management. + +### Events + +```typescript +// Define event classes +class Increment { + constructor(public readonly amount: number = 1) {} +} + +class Decrement { + constructor(public readonly amount: number = 1) {} +} + +class Reset {} + +class SetStep { + constructor(public readonly step: number) {} +} + +type CounterEvent = Increment | Decrement | Reset | SetStep; +``` + +### Bloc Implementation + +```typescript +interface CounterState { + value: number; + step: number; + eventCount: number; +} + +export class CounterBloc extends Bloc { + constructor() { + super({ + value: 0, + step: 1, + eventCount: 0, + }); + + // Register event handlers + this.on(Increment, this.handleIncrement); + this.on(Decrement, this.handleDecrement); + this.on(Reset, this.handleReset); + this.on(SetStep, this.handleSetStep); + } + + private handleIncrement = ( + event: Increment, + emit: (state: CounterState) => void, + ) => { + emit({ + ...this.state, + value: this.state.value + event.amount * this.state.step, + eventCount: this.state.eventCount + 1, + }); + }; + + private handleDecrement = ( + event: Decrement, + emit: (state: CounterState) => void, + ) => { + emit({ + ...this.state, + value: this.state.value - event.amount * this.state.step, + eventCount: this.state.eventCount + 1, + }); + }; + + private handleReset = (_: Reset, emit: (state: CounterState) => void) => { + emit({ + ...this.state, + value: 0, + eventCount: this.state.eventCount + 1, + }); + }; + + private handleSetStep = ( + event: SetStep, + emit: (state: CounterState) => void, + ) => { + emit({ + ...this.state, + step: event.step, + eventCount: this.state.eventCount + 1, + }); + }; + + // Helper methods + increment = (amount?: number) => this.add(new Increment(amount)); + decrement = (amount?: number) => this.add(new Decrement(amount)); + reset = () => this.add(new Reset()); + setStep = (step: number) => this.add(new SetStep(step)); +} +``` + +### Bloc Component + +```tsx +function EventDrivenCounter() { + const [state, bloc] = useBloc(CounterBloc); + + return ( +
+

Event-Driven Counter

+ +
+

{state.value}

+

+ Step: {state.step} | Events: {state.eventCount} +

+
+ +
+ + + + + +
+ +
+ +
+
+ ); +} +``` + +## Persistent Counter + +Counter that saves its state to localStorage. + +```typescript +import { PersistencePlugin } from '@blac/plugin-persistence'; + +class PersistentCounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); + + // Add persistence + this.addPlugin(new PersistencePlugin({ + key: 'counter-state', + })); + } + + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); + reset = () => this.emit({ count: 0 }); +} + +function PersistentCounter() { + const [state, cubit] = useBloc(PersistentCounterCubit); + + return ( +
+

Persistent Counter

+

This counter saves to localStorage!

+

{state.count}

+ + + +

Try refreshing the page!

+
+ ); +} +``` + +## Complete App + +Putting it all together in a complete application: + +```tsx +import React from 'react'; +import { useBloc } from '@blac/react'; +import './CounterApp.css'; + +function CounterApp() { + return ( +
+
+

BlaC Counter Examples

+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ ); +} + +export default CounterApp; +``` + +## Key Takeaways + +1. **Simple API**: Just extend `Cubit` and define methods +2. **Type Safety**: Full TypeScript support out of the box +3. **Flexible Instances**: Shared, isolated, or named instances +4. **Event-Driven**: Use `Bloc` for complex state transitions +5. **Persistence**: Easy to add with plugins +6. **Testing**: State logic is separate from UI + +## Next Steps + +- [Todo List Example](/examples/todo-list) - More complex state management +- [Authentication Example](/examples/authentication) - Async operations +- [API Reference](/api/core/cubit) - Complete API documentation diff --git a/apps/docs/examples/todo-list.md b/apps/docs/examples/todo-list.md new file mode 100644 index 00000000..1e06e6b0 --- /dev/null +++ b/apps/docs/examples/todo-list.md @@ -0,0 +1,768 @@ +# Todo List Example + +A comprehensive todo list application demonstrating real-world BlaC patterns including state management, filtering, persistence, and optimistic updates. + +## Basic Todo List + +Let's start with a simple todo list using a Cubit: + +### State Definition + +```typescript +interface Todo { + id: string; + text: string; + completed: boolean; + createdAt: Date; +} + +interface TodoState { + todos: Todo[]; + filter: 'all' | 'active' | 'completed'; + inputText: string; +} +``` + +### Todo Cubit + +```typescript +import { Cubit } from '@blac/core'; + +export class TodoCubit extends Cubit { + constructor() { + super({ + todos: [], + filter: 'all', + inputText: '', + }); + } + + // Input management + setInputText = (text: string) => { + this.patch({ inputText: text }); + }; + + // Todo operations + addTodo = () => { + const { inputText } = this.state; + if (!inputText.trim()) return; + + const newTodo: Todo = { + id: Date.now().toString(), + text: inputText.trim(), + completed: false, + createdAt: new Date(), + }; + + this.patch({ + todos: [...this.state.todos, newTodo], + inputText: '', + }); + }; + + toggleTodo = (id: string) => { + this.patch({ + todos: this.state.todos.map((todo) => + todo.id === id ? { ...todo, completed: !todo.completed } : todo, + ), + }); + }; + + deleteTodo = (id: string) => { + this.patch({ + todos: this.state.todos.filter((todo) => todo.id !== id), + }); + }; + + editTodo = (id: string, text: string) => { + this.patch({ + todos: this.state.todos.map((todo) => + todo.id === id ? { ...todo, text } : todo, + ), + }); + }; + + // Filter operations + setFilter = (filter: TodoState['filter']) => { + this.patch({ filter }); + }; + + clearCompleted = () => { + this.patch({ + todos: this.state.todos.filter((todo) => !todo.completed), + }); + }; + + // Computed properties + get filteredTodos() { + const { todos, filter } = this.state; + switch (filter) { + case 'active': + return todos.filter((todo) => !todo.completed); + case 'completed': + return todos.filter((todo) => todo.completed); + default: + return todos; + } + } + + get activeTodoCount() { + return this.state.todos.filter((todo) => !todo.completed).length; + } + + get completedTodoCount() { + return this.state.todos.filter((todo) => todo.completed).length; + } + + get allCompleted() { + return this.state.todos.length > 0 && this.activeTodoCount === 0; + } +} +``` + +### React Components + +```tsx +import { useBloc } from '@blac/react'; +import { TodoCubit } from './TodoCubit'; + +export function TodoApp() { + return ( +
+
+

todos

+ +
+
+ + +
+ +
+ ); +} + +function TodoInput() { + const [state, cubit] = useBloc(TodoCubit); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + cubit.addTodo(); + }; + + return ( +
+ cubit.setInputText(e.target.value)} + autoFocus + /> +
+ ); +} + +function ToggleAllCheckbox() { + const [state, cubit] = useBloc(TodoCubit); + + if (state.todos.length === 0) return null; + + const handleToggleAll = () => { + const shouldComplete = cubit.activeTodoCount > 0; + cubit.patch({ + todos: state.todos.map((todo) => ({ + ...todo, + completed: shouldComplete, + })), + }); + }; + + return ( + <> + + + + ); +} + +function TodoList() { + const [_, cubit] = useBloc(TodoCubit); + const todos = cubit.filteredTodos; + + return ( +
    + {todos.map((todo) => ( + + ))} +
+ ); +} + +function TodoItem({ todo }: { todo: Todo }) { + const [_, cubit] = useBloc(TodoCubit); + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(todo.text); + + const handleSave = () => { + if (editText.trim()) { + cubit.editTodo(todo.id, editText.trim()); + setIsEditing(false); + } else { + cubit.deleteTodo(todo.id); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSave(); + } else if (e.key === 'Escape') { + setEditText(todo.text); + setIsEditing(false); + } + }; + + return ( +
  • +
    + cubit.toggleTodo(todo.id)} + /> + +
    + {isEditing && ( + setEditText(e.target.value)} + onBlur={handleSave} + onKeyDown={handleKeyDown} + autoFocus + /> + )} +
  • + ); +} + +function TodoFooter() { + const [state, cubit] = useBloc(TodoCubit); + + if (state.todos.length === 0) return null; + + return ( +
    + + {cubit.activeTodoCount} item + {cubit.activeTodoCount !== 1 ? 's' : ''} left + + + {cubit.completedTodoCount > 0 && ( + + )} +
    + ); +} + +function FilterButtons() { + const [state, cubit] = useBloc(TodoCubit); + const filters: Array = ['all', 'active', 'completed']; + + return ( + + ); +} +``` + +## Advanced: Event-Driven Todo List with Bloc + +For more complex scenarios, use the event-driven Bloc pattern: + +### Events + +```typescript +// Base event class +abstract class TodoEvent { + constructor(public readonly timestamp = Date.now()) {} +} + +// Specific events +class TodoAdded extends TodoEvent { + constructor(public readonly text: string) { + super(); + } +} + +class TodoToggled extends TodoEvent { + constructor(public readonly id: string) { + super(); + } +} + +class TodoDeleted extends TodoEvent { + constructor(public readonly id: string) { + super(); + } +} + +class TodoEdited extends TodoEvent { + constructor( + public readonly id: string, + public readonly text: string, + ) { + super(); + } +} + +class FilterChanged extends TodoEvent { + constructor(public readonly filter: 'all' | 'active' | 'completed') { + super(); + } +} + +class CompletedCleared extends TodoEvent {} + +class AllToggled extends TodoEvent { + constructor(public readonly completed: boolean) { + super(); + } +} + +type TodoEventType = + | TodoAdded + | TodoToggled + | TodoDeleted + | TodoEdited + | FilterChanged + | CompletedCleared + | AllToggled; +``` + +### Todo Bloc + +```typescript +import { Bloc } from '@blac/core'; + +export class TodoBloc extends Bloc { + constructor() { + super({ + todos: [], + filter: 'all', + inputText: '', + }); + + // Register event handlers + this.on(TodoAdded, this.handleTodoAdded); + this.on(TodoToggled, this.handleTodoToggled); + this.on(TodoDeleted, this.handleTodoDeleted); + this.on(TodoEdited, this.handleTodoEdited); + this.on(FilterChanged, this.handleFilterChanged); + this.on(CompletedCleared, this.handleCompletedCleared); + this.on(AllToggled, this.handleAllToggled); + } + + // Event handlers + private handleTodoAdded = ( + event: TodoAdded, + emit: (state: TodoState) => void, + ) => { + const newTodo: Todo = { + id: Date.now().toString(), + text: event.text, + completed: false, + createdAt: new Date(), + }; + + emit({ + ...this.state, + todos: [...this.state.todos, newTodo], + inputText: '', + }); + }; + + private handleTodoToggled = ( + event: TodoToggled, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + todos: this.state.todos.map((todo) => + todo.id === event.id ? { ...todo, completed: !todo.completed } : todo, + ), + }); + }; + + private handleTodoDeleted = ( + event: TodoDeleted, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + todos: this.state.todos.filter((todo) => todo.id !== event.id), + }); + }; + + private handleTodoEdited = ( + event: TodoEdited, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + todos: this.state.todos.map((todo) => + todo.id === event.id ? { ...todo, text: event.text } : todo, + ), + }); + }; + + private handleFilterChanged = ( + event: FilterChanged, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + filter: event.filter, + }); + }; + + private handleCompletedCleared = ( + _: CompletedCleared, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + todos: this.state.todos.filter((todo) => !todo.completed), + }); + }; + + private handleAllToggled = ( + event: AllToggled, + emit: (state: TodoState) => void, + ) => { + emit({ + ...this.state, + todos: this.state.todos.map((todo) => ({ + ...todo, + completed: event.completed, + })), + }); + }; + + // Public methods (convenience wrappers) + addTodo = (text: string) => { + if (text.trim()) { + this.add(new TodoAdded(text.trim())); + } + }; + + toggleTodo = (id: string) => this.add(new TodoToggled(id)); + deleteTodo = (id: string) => this.add(new TodoDeleted(id)); + editTodo = (id: string, text: string) => this.add(new TodoEdited(id, text)); + setFilter = (filter: TodoState['filter']) => + this.add(new FilterChanged(filter)); + clearCompleted = () => this.add(new CompletedCleared()); + toggleAll = (completed: boolean) => this.add(new AllToggled(completed)); + + // Computed properties (same as Cubit version) + get filteredTodos() { + const { todos, filter } = this.state; + switch (filter) { + case 'active': + return todos.filter((todo) => !todo.completed); + case 'completed': + return todos.filter((todo) => todo.completed); + default: + return todos; + } + } + + get activeTodoCount() { + return this.state.todos.filter((todo) => !todo.completed).length; + } + + get completedTodoCount() { + return this.state.todos.filter((todo) => todo.completed).length; + } +} +``` + +## Persistent Todo List + +Add persistence to automatically save and restore todos: + +```typescript +import { PersistencePlugin } from '@blac/plugin-persistence'; + +export class PersistentTodoCubit extends TodoCubit { + constructor() { + super(); + + // Add persistence plugin + this.addPlugin( + new PersistencePlugin({ + key: 'todos-state', + storage: localStorage, + // Optional: transform state before saving + serialize: (state) => ({ + ...state, + // Don't persist input text + inputText: '', + }), + }), + ); + } +} +``` + +## Async Todo List with API + +Handle server synchronization with optimistic updates: + +```typescript +interface AsyncTodoState extends TodoState { + isLoading: boolean; + isSyncing: boolean; + error: string | null; + lastSyncedAt: Date | null; +} + +export class AsyncTodoCubit extends Cubit { + constructor(private api: TodoAPI) { + super({ + todos: [], + filter: 'all', + inputText: '', + isLoading: false, + isSyncing: false, + error: null, + lastSyncedAt: null, + }); + + // Load todos on init + this.loadTodos(); + } + + loadTodos = async () => { + this.patch({ isLoading: true, error: null }); + + try { + const todos = await this.api.getTodos(); + this.patch({ + todos, + isLoading: false, + lastSyncedAt: new Date(), + }); + } catch (error) { + this.patch({ + isLoading: false, + error: error.message, + }); + } + }; + + addTodo = async () => { + const { inputText } = this.state; + if (!inputText.trim()) return; + + const tempTodo: Todo = { + id: `temp-${Date.now()}`, + text: inputText.trim(), + completed: false, + createdAt: new Date(), + }; + + // Optimistic update + this.patch({ + todos: [...this.state.todos, tempTodo], + inputText: '', + isSyncing: true, + }); + + try { + // Create on server + const savedTodo = await this.api.createTodo({ + text: tempTodo.text, + completed: tempTodo.completed, + }); + + // Replace temp todo with saved one + this.patch({ + todos: this.state.todos.map((todo) => + todo.id === tempTodo.id ? savedTodo : todo, + ), + isSyncing: false, + lastSyncedAt: new Date(), + }); + } catch (error) { + // Revert on error + this.patch({ + todos: this.state.todos.filter((todo) => todo.id !== tempTodo.id), + isSyncing: false, + error: `Failed to add todo: ${error.message}`, + }); + } + }; + + toggleTodo = async (id: string) => { + const todo = this.state.todos.find((t) => t.id === id); + if (!todo) return; + + // Optimistic update + this.patch({ + todos: this.state.todos.map((t) => + t.id === id ? { ...t, completed: !t.completed } : t, + ), + isSyncing: true, + }); + + try { + await this.api.updateTodo(id, { completed: !todo.completed }); + this.patch({ + isSyncing: false, + lastSyncedAt: new Date(), + }); + } catch (error) { + // Revert on error + this.patch({ + todos: this.state.todos.map((t) => (t.id === id ? todo : t)), + isSyncing: false, + error: `Failed to update todo: ${error.message}`, + }); + } + }; + + // Batch sync for offline support + syncTodos = async () => { + if (this.state.isSyncing) return; + + this.patch({ isSyncing: true, error: null }); + + try { + // Get latest from server + const serverTodos = await this.api.getTodos(); + + // Merge with local changes (simplified - real app would handle conflicts) + this.patch({ + todos: serverTodos, + isSyncing: false, + lastSyncedAt: new Date(), + }); + } catch (error) { + this.patch({ + isSyncing: false, + error: `Sync failed: ${error.message}`, + }); + } + }; +} +``` + +## Testing + +Testing todo functionality is straightforward: + +```typescript +import { TodoCubit } from './TodoCubit'; + +describe('TodoCubit', () => { + let cubit: TodoCubit; + + beforeEach(() => { + cubit = new TodoCubit(); + }); + + describe('adding todos', () => { + it('should add a new todo', () => { + cubit.setInputText('Test todo'); + cubit.addTodo(); + + expect(cubit.state.todos).toHaveLength(1); + expect(cubit.state.todos[0].text).toBe('Test todo'); + expect(cubit.state.inputText).toBe(''); + }); + + it('should not add empty todos', () => { + cubit.setInputText(' '); + cubit.addTodo(); + + expect(cubit.state.todos).toHaveLength(0); + }); + }); + + describe('filtering', () => { + beforeEach(() => { + cubit.setInputText('Todo 1'); + cubit.addTodo(); + cubit.setInputText('Todo 2'); + cubit.addTodo(); + cubit.toggleTodo(cubit.state.todos[0].id); + }); + + it('should filter active todos', () => { + cubit.setFilter('active'); + expect(cubit.filteredTodos).toHaveLength(1); + expect(cubit.filteredTodos[0].text).toBe('Todo 2'); + }); + + it('should filter completed todos', () => { + cubit.setFilter('completed'); + expect(cubit.filteredTodos).toHaveLength(1); + expect(cubit.filteredTodos[0].text).toBe('Todo 1'); + }); + }); + + describe('computed properties', () => { + it('should calculate counts correctly', () => { + cubit.setInputText('Todo 1'); + cubit.addTodo(); + cubit.setInputText('Todo 2'); + cubit.addTodo(); + cubit.toggleTodo(cubit.state.todos[0].id); + + expect(cubit.activeTodoCount).toBe(1); + expect(cubit.completedTodoCount).toBe(1); + }); + }); +}); +``` + +## Key Patterns Demonstrated + +1. **State Structure**: Well-organized state with clear types +2. **Computed Properties**: Derived values using getters +3. **Optimistic Updates**: Update UI immediately, sync in background +4. **Error Handling**: Graceful error states with rollback +5. **Event-Driven Architecture**: Clear audit trail with Bloc pattern +6. **Persistence**: Automatic save/restore with plugins +7. **Testing**: Easy unit tests for business logic +8. **Performance**: Automatic optimization with BlaC's proxy system + +## Next Steps + +- [Authentication Example](/examples/authentication) - User auth flows +- [Form Management](/examples/forms) - Complex form handling +- [Real-time Updates](/examples/real-time) - WebSocket integration +- [API Reference](/api/core/cubit) - Complete API documentation diff --git a/apps/docs/getting-started/async-operations.md b/apps/docs/getting-started/async-operations.md new file mode 100644 index 00000000..4247762b --- /dev/null +++ b/apps/docs/getting-started/async-operations.md @@ -0,0 +1,347 @@ +# Async Operations + +Real-world applications need to fetch data, save to APIs, and handle other asynchronous operations. BlaC makes this straightforward while maintaining clean, testable code. + +## Loading States Pattern + +The key to good async handling is explicit state management. Always track: + +- Loading status +- Success data +- Error states + +```typescript +// src/cubits/UserCubit.ts +import { Cubit } from '@blac/core'; + +interface UserState { + user: User | null; + isLoading: boolean; + error: string | null; +} + +interface User { + id: string; + name: string; + email: string; +} + +export class UserCubit extends Cubit { + constructor() { + super({ + user: null, + isLoading: false, + error: null, + }); + } + + fetchUser = async (userId: string) => { + // Start loading + this.patch({ isLoading: true, error: null }); + + try { + // Simulate API call + const response = await fetch(`/api/users/${userId}`); + + if (!response.ok) { + throw new Error('Failed to fetch user'); + } + + const user = await response.json(); + + // Success - update state with data + this.patch({ + user, + isLoading: false, + error: null, + }); + } catch (error) { + // Error - update state with error message + this.patch({ + user: null, + isLoading: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + }; + + clearError = () => { + this.patch({ error: null }); + }; +} +``` + +## Using Async Cubits in React + +```tsx +// src/components/UserProfile.tsx +import { useBloc } from '@blac/react'; +import { UserCubit } from '../cubits/UserCubit'; +import { useEffect } from 'react'; + +export function UserProfile({ userId }: { userId: string }) { + const [state, cubit] = useBloc(UserCubit); + + // Fetch user when component mounts or userId changes + useEffect(() => { + cubit.fetchUser(userId); + }, [userId, cubit]); + + if (state.isLoading) { + return
    Loading user...
    ; + } + + if (state.error) { + return ( +
    +

    Error: {state.error}

    + +
    + ); + } + + if (!state.user) { + return
    No user data
    ; + } + + return ( +
    +

    {state.user.name}

    +

    {state.user.email}

    +
    + ); +} +``` + +## Advanced Patterns + +### Cancellation + +Prevent race conditions when rapid requests occur: + +```typescript +export class SearchCubit extends Cubit { + private abortController?: AbortController; + + search = async (query: string) => { + // Cancel previous request + this.abortController?.abort(); + this.abortController = new AbortController(); + + this.patch({ isLoading: true, error: null }); + + try { + const response = await fetch(`/api/search?q=${query}`, { + signal: this.abortController.signal, + }); + + const results = await response.json(); + this.patch({ results, isLoading: false }); + } catch (error) { + // Ignore abort errors + if (error instanceof Error && error.name === 'AbortError') { + return; + } + + this.patch({ + isLoading: false, + error: error instanceof Error ? error.message : 'Search failed', + }); + } + }; + + // Clean up on disposal + dispose() { + this.abortController?.abort(); + super.dispose(); + } +} +``` + +### Optimistic Updates + +Update UI immediately for better UX: + +```typescript +export class TodoCubit extends Cubit { + toggleTodo = async (id: string) => { + const todo = this.state.todos.find((t) => t.id === id); + if (!todo) return; + + // Optimistic update + const optimisticTodos = this.state.todos.map((t) => + t.id === id ? { ...t, completed: !t.completed } : t, + ); + this.patch({ todos: optimisticTodos }); + + try { + // API call + await fetch(`/api/todos/${id}/toggle`, { method: 'POST' }); + // Success - state already updated + } catch (error) { + // Revert on error + this.patch({ + todos: this.state.todos, + error: 'Failed to update todo', + }); + } + }; +} +``` + +### Pagination + +Handle paginated data elegantly: + +```typescript +interface PaginatedState { + items: T[]; + currentPage: number; + totalPages: number; + isLoading: boolean; + error: string | null; +} + +export class ProductsCubit extends Cubit> { + constructor() { + super({ + items: [], + currentPage: 1, + totalPages: 1, + isLoading: false, + error: null, + }); + } + + loadPage = async (page: number) => { + this.patch({ isLoading: true, error: null }); + + try { + const response = await fetch(`/api/products?page=${page}`); + const data = await response.json(); + + this.patch({ + items: data.items, + currentPage: page, + totalPages: data.totalPages, + isLoading: false, + }); + } catch (error) { + this.patch({ + isLoading: false, + error: 'Failed to load products', + }); + } + }; + + nextPage = () => { + if (this.state.currentPage < this.state.totalPages) { + this.loadPage(this.state.currentPage + 1); + } + }; + + previousPage = () => { + if (this.state.currentPage > 1) { + this.loadPage(this.state.currentPage - 1); + } + }; +} +``` + +## Best Practices + +### 1. Always Handle All States + +Never leave your UI in an undefined state: + +```typescript +// ❌ Bad - what if loading or error? +if (state.data) { + return
    {state.data.name}
    ; +} + +// ✅ Good - handle all cases +if (state.isLoading) return ; +if (state.error) return ; +if (!state.data) return ; +return
    {state.data.name}
    ; +``` + +### 2. Separate API Logic + +Keep API calls separate from your Cubits: + +```typescript +// src/api/userApi.ts +export const userApi = { + async getUser(id: string): Promise { + const response = await fetch(`/api/users/${id}`); + if (!response.ok) throw new Error('Failed to fetch'); + return response.json(); + }, +}; + +// In your Cubit +fetchUser = async (id: string) => { + this.patch({ isLoading: true }); + try { + const user = await userApi.getUser(id); + this.patch({ user, isLoading: false }); + } catch (error) { + // ... + } +}; +``` + +### 3. Use TypeScript for API Responses + +Define types for your API responses: + +```typescript +interface ApiResponse { + data: T; + status: 'success' | 'error'; + message?: string; +} + +interface PaginatedResponse { + items: T[]; + page: number; + totalPages: number; + totalItems: number; +} +``` + +## Testing Async Operations + +Async Cubits are easy to test: + +```typescript +import { UserCubit } from './UserCubit'; + +describe('UserCubit', () => { + it('should fetch user successfully', async () => { + const cubit = new UserCubit(); + + // Mock fetch + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: '1', name: 'John' }), + }); + + await cubit.fetchUser('1'); + + expect(cubit.state).toEqual({ + user: { id: '1', name: 'John' }, + isLoading: false, + error: null, + }); + }); +}); +``` + +## What's Next? + +- [First Bloc](/getting-started/first-bloc) - Event-driven async operations +- [Error Handling](/patterns/error-handling) - Advanced error patterns +- [Testing](/patterns/testing) - Testing strategies for async code diff --git a/apps/docs/getting-started/first-bloc.md b/apps/docs/getting-started/first-bloc.md new file mode 100644 index 00000000..9be1c2c9 --- /dev/null +++ b/apps/docs/getting-started/first-bloc.md @@ -0,0 +1,196 @@ +# Your First Bloc + +Blocs provide event-driven state management for more complex scenarios. To demonstrate, we'll build a simple counter app using Blocs and events. + +## When to Use Blocs + +**Use Cubit when:** + +- Simple state updates +- Direct method calls are fine + +**Use Bloc when:** + +- You want a clear audit trail +- Multiple events affect the same state +- Complex async operations + +## Basic Example + +### 1. Define Events + +```typescript +// Events are simple classes +export class Increment {} +export class Decrement {} +export class Reset {} +``` + +### 2. Create the Bloc + +```typescript +import { Bloc } from '@blac/core'; + +interface CounterState { + count: number; +} + +type CounterEvent = Increment | Decrement | Reset; + +export class CounterBloc extends Bloc { + constructor() { + super({ count: 0 }); + + // Register event handlers + this.on(Increment, this.handleIncrement); + this.on(Decrement, this.handleDecrement); + this.on(Reset, this.handleReset); + } + + // IMPORTANT: Always use arrow functions for methods! + private handleIncrement = ( + event: Increment, + emit: (state: CounterState) => void, + ) => { + emit({ count: this.state.count + 1 }); + }; + + private handleDecrement = ( + event: Decrement, + emit: (state: CounterState) => void, + ) => { + emit({ count: this.state.count - 1 }); + }; + + private handleReset = (event: Reset, emit: (state: CounterState) => void) => { + emit({ count: 0 }); + }; + + // Helper methods for convenience + increment = () => this.add(new Increment()); + decrement = () => this.add(new Decrement()); + reset = () => this.add(new Reset()); +} +``` + +### 3. Use in React + +```tsx +import { useBloc } from '@blac/react'; +import { CounterBloc } from './CounterBloc'; + +export function Counter() { + const [state, counterBloc] = useBloc(CounterBloc); + + return ( +
    +

    Count: {state.count}

    + + + +
    + ); +} +``` + +## Key Concepts + +:::warning Critical: Arrow Functions Required +Just like with Cubits, you MUST use arrow function syntax for all Bloc methods and event handlers: + +```typescript +// ✅ CORRECT +handleIncrement = (event, emit) => { ... }; +increment = () => this.add(new Increment()); + +// ❌ WRONG - Will break in React +handleIncrement(event, emit) { ... } +increment() { this.add(new Increment()); } +``` + +::: + +### Events vs Methods + +```typescript +// Cubit: Direct method calls +cubit.increment(); + +// Bloc: Events +bloc.add(new Increment()); +// Or use helper methods +bloc.increment(); // which calls this.add(new Increment()) +``` + +### Event Handlers + +Event handlers receive: + +- The event instance +- An `emit` function to update state + +```typescript +private handleIncrement = (event: Increment, emit: (state: CounterState) => void) => { + // Access current state: this.state + // Access event data: event.someProperty + // Update state: emit(newState) + emit({ count: this.state.count + 1 }); +}; +``` + +### Events with Data + +```typescript +export class IncrementBy { + constructor(public readonly amount: number) {} +} + +// Usage +bloc.add(new IncrementBy(5)); + +// Handler +private handleIncrementBy = (event: IncrementBy, emit: (state: CounterState) => void) => { + emit({ count: this.state.count + event.amount }); +}; +``` + +## Benefits + +1. **Audit Trail**: Every state change has a corresponding event +2. **Debugging**: Log all events to see exactly what happened +3. **Testing**: Dispatch specific events and verify state changes +4. **Time Travel**: Replay events to recreate states + +## Simple Async Example + +```typescript +export class LoadUser { + constructor(public readonly userId: string) {} +} + +export class UserLoaded { + constructor(public readonly user: User) {} +} + +export class LoadUserFailed { + constructor(public readonly error: string) {} +} + +// Handler +private handleLoadUser = async (event: LoadUser, emit: (state: UserState) => void) => { + emit({ ...this.state, isLoading: true }); + + try { + const user = await api.getUser(event.userId); + this.add(new UserLoaded(user)); + } catch (error) { + this.add(new LoadUserFailed(error.message)); + } +}; +``` + +## What's Next? + +- [React Patterns](/react/patterns) - Advanced component patterns +- [API Reference](/api/core/bloc) - Complete Bloc API +- [Testing](/patterns/testing) - How to test Blocs diff --git a/apps/docs/getting-started/first-cubit.md b/apps/docs/getting-started/first-cubit.md new file mode 100644 index 00000000..5dc8a73a --- /dev/null +++ b/apps/docs/getting-started/first-cubit.md @@ -0,0 +1,211 @@ +# Your First Cubit + +Let's learn Cubits by building a simple counter. This focuses on the core concepts without overwhelming complexity. + +## What is a Cubit? + +A Cubit is a simple state container that: + +- Holds a single piece of state +- Provides methods to update that state +- Notifies listeners when state changes +- Lives outside your React components + +Think of it as a self-contained box of logic that your UI can connect to. + +## Building a Counter + +Let's build a counter step by step to understand how Cubits work. + +### Step 1: Create the Cubit + +```typescript +// src/CounterCubit.ts +import { Cubit } from '@blac/core'; + +interface CounterState { + count: number; +} + +export class CounterCubit extends Cubit { + constructor() { + super({ count: 0 }); + } + + increment = () => { + this.emit({ count: this.state.count + 1 }); + }; + + decrement = () => { + this.emit({ count: this.state.count - 1 }); + }; + + reset = () => { + this.emit({ count: 0 }); + }; +} +``` + +### Key Concepts + +Let's break down what's happening: + +1. **State Type**: We define an interface to describe our state shape +2. **Initial State**: The constructor calls `super()` with the starting state `{ count: 0 }` +3. **Arrow Functions**: All methods use arrow function syntax to maintain proper `this` binding +4. **emit() Method**: Updates the entire state with a new object + +:::warning Critical: Arrow Functions Required +You MUST use arrow function syntax (`method = () => {}`) for all Cubit/Bloc methods. Regular methods (`method() {}`) will break in React due to `this` binding issues: + +```typescript +// ✅ CORRECT - Arrow function +increment = () => { + this.emit({ count: this.state.count + 1 }); +}; + +// ❌ WRONG - Will throw "Cannot read property 'emit' of undefined" +increment() { + this.emit({ count: this.state.count + 1 }); +} +``` + +::: + +### Step 2: Use the Cubit in React + +Now let's create a component that uses our CounterCubit: + +```tsx +// src/Counter.tsx +import { useBloc } from '@blac/react'; +import { CounterCubit } from './CounterCubit'; + +export function Counter() { + const [state, counter] = useBloc(CounterCubit); + + return ( +
    +

    Count: {state.count}

    + + + +
    + ); +} +``` + +### Step 3: Understanding the Flow + +Here's what happens when a user clicks the "+" button: + +1. **User clicks "+"** → `counter.increment()` is called +2. **Cubit updates state** → `emit()` creates new state `{ count: 1 }` +3. **React re-renders** → `useBloc` detects state change +4. **UI updates** → Counter displays the new count + +This unidirectional flow makes debugging easy and state changes predictable. + +## Adding More Features + +Once you understand the basics, you can extend your counter: + +### Adding More State + +```typescript +interface CounterState { + count: number; + step: number; // How much to increment/decrement by +} + +export class CounterCubit extends Cubit { + constructor() { + super({ count: 0, step: 1 }); + } + + increment = () => { + this.emit({ + count: this.state.count + this.state.step, + step: this.state.step, + }); + }; + + setStep = (step: number) => { + this.emit({ + count: this.state.count, + step: step, + }); + }; +} +``` + +### Using patch() for Partial Updates + +Instead of `emit()`, you can use `patch()` to update only specific properties: + +```typescript +increment = () => { + this.patch({ count: this.state.count + this.state.step }); +}; + +setStep = (step: number) => { + this.patch({ step }); +}; +``` + +### Adding Computed Properties + +```typescript +export class CounterCubit extends Cubit { + // ... methods ... + + get isEven() { + return this.state.count % 2 === 0; + } + + get isPositive() { + return this.state.count > 0; + } +} + +// Use in React +function Counter() { + const [state, counter] = useBloc(CounterCubit); + + return ( +
    +

    Count: {state.count}

    +

    {counter.isEven ? 'Even' : 'Odd'}

    +

    {counter.isPositive ? 'Positive' : 'Zero or Negative'}

    +
    + ); +} +``` + +## What You've Learned + +Congratulations! You've now: + +- ✅ Created your first Cubit +- ✅ Managed state with `emit()` and `patch()` +- ✅ Connected a Cubit to React components with `useBloc` +- ✅ Understood the unidirectional data flow +- ✅ Added computed properties with getters + +## What's Next? + +Ready to level up? Learn about Blocs for event-driven state management: + + diff --git a/apps/docs/getting-started/installation.md b/apps/docs/getting-started/installation.md new file mode 100644 index 00000000..9da538d6 --- /dev/null +++ b/apps/docs/getting-started/installation.md @@ -0,0 +1,160 @@ +# Installation + +Getting started with BlaC is straightforward. This guide will walk you through installation and basic setup. + +## Prerequisites + +- Node.js 16.0 or later +- React 16.8 or later (for hooks support) +- TypeScript 4.0 or later (optional but recommended) + +## Install BlaC + +BlaC is distributed as two packages: + +- `@blac/core` - The core state management engine +- `@blac/react` - React integration with hooks + +For React applications, install the React package which includes core: + +::: code-group + +```bash [npm] +npm install @blac/react +``` + +```bash [yarn] +yarn add @blac/react +``` + +```bash [pnpm] +pnpm add @blac/react +``` + +::: + +## TypeScript Configuration + +BlaC is built with TypeScript and provides excellent type safety out of the box. If you're using TypeScript, ensure your `tsconfig.json` includes: + +```json +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "jsx": "react-jsx" // or "react" for older React versions + } +} +``` + +## Project Structure + +We recommend organizing your BlaC code in a dedicated directory: + +``` +src/ +├── state/ # BlaC state containers +│ ├── auth/ # Feature-specific folders +│ │ ├── auth.cubit.ts +│ │ └── auth.types.ts +│ └── todo/ +│ ├── todo.bloc.ts +│ ├── todo.events.ts +│ └── todo.types.ts +├── components/ # React components +└── App.tsx +``` + +## Verify Installation + +Create a simple counter to verify everything is working: + +```typescript +// src/state/counter.cubit.ts +import { Cubit } from '@blac/core'; + +export class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); + } + + // IMPORTANT: Always use arrow functions for methods! + // This ensures proper 'this' binding when used in React + increment = () => this.emit({ count: this.state.count + 1 }); + decrement = () => this.emit({ count: this.state.count - 1 }); + reset = () => this.emit({ count: 0 }); +} +``` + +```tsx +// src/App.tsx +import { useBloc } from '@blac/react'; +import { CounterCubit } from './state/counter.cubit'; + +function App() { + const [state, counter] = useBloc(CounterCubit); + + return ( +
    +

    Count: {state.count}

    + + + +
    + ); +} + +export default App; +``` + +If you see the counter working, congratulations! You've successfully installed BlaC. + +## Optional: Global Configuration + +BlaC works out of the box with sensible defaults, but you can customize its behavior: + +```typescript +// src/index.tsx or src/main.tsx +import { Blac } from '@blac/core'; + +// Configure before your app renders +Blac.setConfig({ + // Control automatic render optimization + proxyDependencyTracking: true, +}); + +// Enable logging for debugging +if (process.env.NODE_ENV === 'development') { + Blac.enableLog = true; +} + +// Then render your app +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); +``` + +## What's Next? + +Now that you have BlaC installed, let's create your first Cubit: + + diff --git a/apps/docs/getting-started/learning-paths.md b/apps/docs/getting-started/learning-paths.md new file mode 100644 index 00000000..3a70e655 --- /dev/null +++ b/apps/docs/getting-started/learning-paths.md @@ -0,0 +1,207 @@ +# Learning Paths + +Master BlaC state management with our structured learning paths designed for different skill levels and use cases. + +## 🚀 Quick Start Path + +**For developers who want to get up and running quickly** + +### 1. Installation & Setup (5 min) + +Start with [Installation](/getting-started/installation) to set up BlaC in your project. + +### 2. Your First Cubit (10 min) + +Learn the basics with [Your First Cubit](/getting-started/first-cubit) - create a simple counter. + +### 3. React Integration (8 min) + +Connect your state to React components using the [useBloc hook](/api/react/use-bloc). + +### 4. Try Interactive Examples (15 min) + +Explore live demos in the [BlaC Playground](https://blac-playground.vercel.app/demos) to see patterns in action. + +## 📚 Fundamentals Path + +**For developers who want to understand core concepts** + +### Week 1: Core Concepts + +- [What is BlaC?](/introduction) - Philosophy and architecture +- [State Management](/concepts/state-management) - How BlaC manages state +- [Cubits](/concepts/cubits) - Simple state containers +- [Your First Cubit](/getting-started/first-cubit) - Hands-on practice + +### Week 2: Event-Driven Architecture + +- [Blocs](/concepts/blocs) - Event-driven state management +- [Your First Bloc](/getting-started/first-bloc) - Build an event-driven component +- [Async Operations](/getting-started/async-operations) - Handle async workflows + +### Week 3: React Integration + +- [React Hooks](/react/hooks) - useBloc and useExternalBlocStore +- [React Patterns](/react/patterns) - Best practices for React apps +- [Instance Management](/concepts/instance-management) - Shared vs isolated instances + +## 🔥 Advanced Path + +**For developers building complex applications** + +### Performance Optimization + +1. **Selectors & Memoization** + - Understanding proxy-based dependency tracking + - Writing efficient selectors + - Preventing unnecessary re-renders + +2. **Instance Management Strategies** + - When to use shared vs isolated instances + - Keep-alive patterns for persistent state + - Props-based Blocs for dynamic instances + +3. **Testing & Debugging** + - Unit testing Cubits and Blocs + - Integration testing with React + - Using the RenderLoggingPlugin + +### Plugin Development + +1. **Plugin System Overview** + - [Understanding the plugin architecture](/plugins/overview) + - [System vs Bloc plugins](/plugins/system-plugins) + +2. **Built-in Plugins** + - [Persistence Plugin](/plugins/persistence) - Auto-save state + - Creating custom logging plugins + +3. **Custom Plugin Development** + - [Creating your own plugins](/plugins/creating-plugins) + - Lifecycle hooks and events + - Best practices + +### Architecture Patterns + +1. **Composition Patterns** + - Composing multiple Blocs + - Parent-child state relationships + - Cross-cutting concerns + +2. **Error Handling** + - Global error boundaries + - Bloc-level error handling + - Recovery strategies + +3. **Real-World Patterns** + - Authentication flows + - Form management + - Data fetching and caching + +## 🎯 Migration Path + +**For teams migrating from other state management solutions** + +### From Redux + +1. Compare concepts: [BlaC vs Redux](/comparisons#redux) +2. Map Redux patterns to BlaC patterns +3. Gradual migration strategy +4. [See migration example](https://blac-playground.vercel.app/demos/migration/from-redux) + +### From MobX + +1. Compare reactivity models: [BlaC vs MobX](/comparisons#mobx) +2. Convert observables to Cubits +3. Handle computed values +4. [See migration example](https://blac-playground.vercel.app/demos/migration/from-mobx) + +### From Context API + +1. Compare simplicity: [BlaC vs Context](/comparisons#context-api) +2. Replace providers with Blocs +3. Improve performance +4. [See migration example](https://blac-playground.vercel.app/demos/migration/from-context) + +## 📖 Reference Path + +**For quick lookups and API reference** + +### Core APIs + +- [Blac](/api/core/blac) - Global configuration +- [Cubit](/api/core/cubit) - Simple state container +- [Bloc](/api/core/bloc) - Event-driven container +- [BlocBase](/api/core/bloc-base) - Base class + +### React APIs + +- [useBloc](/api/react/use-bloc) - Primary React hook +- [useExternalBlocStore](/api/react/use-external-bloc-store) - External instances + +### Plugin APIs + +- [Plugin API Reference](/plugins/api-reference) +- [Creating Plugins](/plugins/creating-plugins) + +## 🚢 Production Path + +**For teams preparing for production** + +### Pre-Production Checklist + +- [ ] Error handling strategy implemented +- [ ] Performance monitoring in place +- [ ] State persistence configured +- [ ] Testing coverage > 80% +- [ ] Documentation updated + +### Best Practices + +- [Testing strategies](/patterns/testing) +- [Performance optimization](/patterns/performance) +- [Error handling](/patterns/error-handling) +- [State persistence](/patterns/persistence) + +### Monitoring & Debugging + +- Using the RenderLoggingPlugin +- Performance profiling +- State debugging techniques +- Production error tracking + +## 🎓 Interactive Learning + +### Try the Playground + +The [BlaC Playground](https://blac-playground.vercel.app) offers: + +- **Live Demos**: See code and results side-by-side +- **Interactive Editor**: Modify examples in real-time +- **Performance Metrics**: Understand render optimization + +### Community Resources + +- [GitHub Discussions](https://github.com/jsnanigans/blac/discussions) - Ask questions +- [Discord Community](#) - Chat with other developers +- [Stack Overflow](https://stackoverflow.com/questions/tagged/blac) - Find answers + +## Next Steps + +Ready to start? Here are your options: + +
    + +**New to BlaC?** Start with the [Quick Start Path](#quick-start-path) + +**Know the basics?** Jump to the [Advanced Path](#advanced-path) + +**Migrating a project?** Check the [Migration Path](#migration-path) + +**Need specific info?** Use the [Reference Path](#reference-path) + +
    + +--- + +Remember: The best way to learn is by doing. Open the [Playground](https://blac-playground.vercel.app) in another tab and practice as you learn! diff --git a/apps/docs/index.md b/apps/docs/index.md index 27664c4b..2351d0b3 100644 --- a/apps/docs/index.md +++ b/apps/docs/index.md @@ -1,120 +1,101 @@ --- -# https://vitepress.dev/reference/default-theme-home-page layout: home -title: Blac - Beautiful State Management for React -description: Lightweight, flexible, and predictable state management for modern React applications. -head: - - - meta - - name: description - content: Blac is a lightweight, flexible, and predictable state management library for React applications. - - - meta - - name: keywords - content: blac, react, state management, bloc, cubit, typescript, reactive, predictable state - - - meta - - property: og:title - content: Blac - Beautiful State Management for React - - - meta - - property: og:description - content: Lightweight, flexible, and predictable state management for modern React applications. - - - meta - - property: og:type - content: website - - - meta - - property: og:image - content: /logo.svg - - - meta - - name: twitter:card - content: summary - - - meta - - name: twitter:title - content: Blac - Beautiful State Management for React - - - meta - - name: twitter:description - content: Lightweight, flexible, and predictable state management for modern React applications. ---- - -# Blac -
    Lightweight, flexible, and predictable state management for modern React applications.
    - -
    - Blac Logo -
    +hero: + name: BlaC + text: Business Logic as Components + tagline: Simple, powerful state management for React with zero boilerplate + image: + src: /logo.svg + alt: BlaC + actions: + - theme: brand + text: Get Started + link: /introduction + - theme: alt + text: View on GitHub + link: https://github.com/jsnanigans/blac + +features: + - icon: ⚡ + title: Zero Boilerplate + details: Get started in seconds. No actions, reducers, or complex setup required. + - icon: 🎯 + title: Type-Safe by Default + details: Full TypeScript support with perfect type inference and autocompletion. + - icon: 🚀 + title: Optimized Performance + details: Automatic render optimization through intelligent dependency tracking. + - icon: 🧩 + title: Flexible Architecture + details: Scale from simple Cubits to complex event-driven Blocs as needed. +--- - + -## Quick Example +## Quick Start -```tsx -// 1. Define your Cubit (e.g., in src/cubits/CounterCubit.ts) +```typescript +// 1. Define your state container import { Cubit } from '@blac/core'; -interface CounterState { - count: number; -} - -export class CounterCubit extends Cubit { +class CounterCubit extends Cubit<{ count: number }> { constructor() { - super({ count: 0 }); // Initial state + super({ count: 0 }); } - // Methods must be arrow functions! + // Important: Use arrow functions for all methods! increment = () => this.emit({ count: this.state.count + 1 }); decrement = () => this.emit({ count: this.state.count - 1 }); } -// 2. Use the Cubit in your React component +// 2. Use it in your React component import { useBloc } from '@blac/react'; -import { CounterCubit } from '../cubits/CounterCubit'; // Adjust path -function MyCounter() { - const [state, counterCubit] = useBloc(CounterCubit); +function Counter() { + const [state, cubit] = useBloc(CounterCubit); return (
    -

    Count: {state.count}

    - - + + {state.count} +
    ); } - -export default MyCounter; ``` +That's it. No providers, no boilerplate, just clean state management. + + diff --git a/apps/docs/introduction.md b/apps/docs/introduction.md new file mode 100644 index 00000000..47b3184f --- /dev/null +++ b/apps/docs/introduction.md @@ -0,0 +1,121 @@ +# Introduction + +## What is BlaC? + +BlaC (Business Logic as Components) is a modern state management library for React that brings clarity, predictability, and simplicity to your applications. Born from the desire to separate business logic from UI components, BlaC makes your code more testable, maintainable, and easier to reason about. + +At its core, BlaC provides: + +- **Clear separation of concerns** between business logic and UI +- **Type-safe state management** with full TypeScript support +- **Minimal boilerplate** compared to traditional solutions +- **Automatic optimization** for React re-renders +- **Flexible architecture** that scales from simple to complex applications + +## Why BlaC? + +### The Problem with Traditional State Management + +Many React applications suffer from: + +- Business logic scattered throughout components +- Difficult-to-test stateful components +- Complex state management setups with excessive boilerplate +- Performance issues from unnecessary re-renders +- Type safety challenges with dynamic state + +### The BlaC Solution + +BlaC addresses these challenges by introducing state containers (Cubits and Blocs) that encapsulate your business logic separately from your UI components. This separation brings numerous benefits: + +1. **Testability**: Test your business logic in isolation without rendering components +2. **Reusability**: Share state logic across different UI implementations +3. **Maintainability**: Modify business logic without touching UI code +4. **Performance**: Automatic render optimization through smart dependency tracking +5. **Developer Experience**: Full TypeScript support with excellent IDE integration + +## Core Philosophy + +### Business Logic as Components + +Just as React revolutionized UI development by thinking in components, BlaC applies the same principle to business logic. Each piece of business logic becomes a self-contained, reusable component with: + +- **Clear boundaries**: Each state container manages a specific domain +- **Explicit dependencies**: Props and state types are clearly defined +- **Predictable behavior**: State changes follow a unidirectional flow + +### Simplicity First + +BlaC prioritizes developer experience without sacrificing power: + +```typescript +// This is all you need for a working state container +class CounterCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); + } + + increment = () => this.emit({ count: this.state.count + 1 }); +} + +// And this is how you use it +function Counter() { + const [state, cubit] = useBloc(CounterCubit); + return ; +} +``` + +### Progressive Complexity + +Start simple with Cubits for basic state management, then graduate to Blocs when you need event-driven architecture: + +- **Cubits**: Direct state updates via `emit()` and `patch()` +- **Blocs**: Event-driven state transitions with type-safe event handlers + +## When to Use BlaC + +BlaC shines in applications that need: + +- **Clean architecture** with separated concerns +- **Complex state logic** that would clutter components +- **Shared state** across multiple components +- **Testable business logic** independent of UI +- **Type-safe state management** with TypeScript +- **Performance optimization** for frequent state updates + +## How BlaC Compares + +BlaC addresses common pain points found in other state management solutions while maintaining a simple, intuitive API. Whether you're coming from Redux's boilerplate-heavy approach, MobX's magical reactivity, Context API's performance limitations, or Zustand's manual selectors, BlaC offers a refreshing alternative. + +For a detailed comparison with Redux, MobX, Context API, Zustand, and other popular solutions, see our [Comparison Guide](/comparisons). + +## Architecture Overview + +BlaC consists of two main packages working in harmony: + +### @blac/core + +The foundation providing: + +- **State Containers**: `Cubit` and `Bloc` base classes +- **Instance Management**: Automatic creation, sharing, and disposal +- **Plugin System**: Extensible architecture for custom features + +### @blac/react + +The React integration offering: + +- **useBloc Hook**: Connect components to state containers +- **Dependency Tracking**: Automatic optimization of re-renders +- **React Patterns**: Best practices for React applications + +## What's Next? + +Ready to dive in? Here's your learning path: + +1. **[Getting Started](/getting-started/installation)**: Install BlaC and create your first Cubit +2. **[Core Concepts](/concepts/state-management)**: Understand the fundamental principles +3. **[API Reference](/api/core/cubit)**: Explore the complete API surface +4. **[Examples](/examples/)**: Learn from practical, real-world examples + +Welcome to BlaC! Let's build better React applications together. diff --git a/apps/docs/learn/architecture.md b/apps/docs/learn/architecture.md index 8f98b0f9..7a4b30fb 100644 --- a/apps/docs/learn/architecture.md +++ b/apps/docs/learn/architecture.md @@ -6,26 +6,26 @@ Understanding the architecture of Blac helps in leveraging its full potential fo Blac deliberately uses ES6 classes for defining `Bloc` and `Cubit` state containers. This approach offers several advantages in the context of state management and aligns well with TypeScript: -- **Identifier for Instances**: The class definition itself (e.g., `UserBloc` or `CounterCubit`) acts as a primary, human-readable key for Blac's internal instance manager. This allows Blac to uniquely identify, retrieve, and manage shared or isolated instances of state containers. +- **Identifier for Instances**: The class definition itself (e.g., `UserBloc` or `CounterCubit`) acts as a primary, human-readable key for Blac's internal instance manager. This allows Blac to uniquely identify, retrieve, and manage shared or isolated instances of state containers. -- **Clear Structure and Encapsulation**: Classes provide a natural and well-understood way to: - * Define the shape of the state using generic type parameters (e.g., `Cubit`). - * Initialize the state within the `constructor`. - * Encapsulate related business logic as methods within the same unit. +- **Clear Structure and Encapsulation**: Classes provide a natural and well-understood way to: + - Define the shape of the state using generic type parameters (e.g., `Cubit`). + - Initialize the state within the `constructor`. + - Encapsulate related business logic as methods within the same unit. -- **TypeScript Benefits**: The class-based approach integrates seamlessly with TypeScript, enabling: - * Strong typing for state, constructor props, and methods, enhancing code reliability and maintainability. - * Correct `this` context binding when instance methods are defined as arrow functions (e.g., `increment = () => ...`), which is crucial for their use as event handlers or when passed as callbacks. - * Visibility modifiers (`public`, `private`, `protected`) if needed, though often not strictly necessary for typical Blac usage. +- **TypeScript Benefits**: The class-based approach integrates seamlessly with TypeScript, enabling: + - Strong typing for state, constructor props, and methods, enhancing code reliability and maintainability. + - Correct `this` context binding when instance methods are defined as arrow functions (e.g., `increment = () => ...`), which is crucial for their use as event handlers or when passed as callbacks. + - Visibility modifiers (`public`, `private`, `protected`) if needed, though often not strictly necessary for typical Blac usage. -- **Static Properties for Configuration**: Classes allow the use of `static` properties to declaratively configure the behavior of `Bloc`s and `Cubit`s. This includes: - * `static isolated = true;`: To mark a Bloc/Cubit for isolated instance management. - * `static keepAlive = true;`: To prevent a shared instance from being disposed of when it has no active listeners. - * `static addons = [/* ... */];`: To attach addons like `Persist` for enhanced functionality. +- **Static Properties for Configuration**: Classes allow the use of `static` properties to declaratively configure the behavior of `Bloc`s and `Cubit`s. This includes: + - `static isolated = true;`: To mark a Bloc/Cubit for isolated instance management. + - `static keepAlive = true;`: To prevent a shared instance from being disposed of when it has no active listeners. + - `static addons = [/* ... */];`: To attach addons like `Persist` for enhanced functionality. -- **Extensibility and Reusability**: Standard Object-Oriented Programming (OOP) patterns like inheritance can be used if desired, allowing for the creation of base Blocs/Cubits with common functionality that can be extended by more specialized ones. However, composition using addons is often a more flexible approach for adding features. +- **Extensibility and Reusability**: Standard Object-Oriented Programming (OOP) patterns like inheritance can be used if desired, allowing for the creation of base Blocs/Cubits with common functionality that can be extended by more specialized ones. However, composition using addons is often a more flexible approach for adding features. -- **Testability**: State containers defined as classes have a clear public API (constructor, methods). This makes them straightforward to instantiate, interact with, and assert against in unit tests, independent of the UI. +- **Testability**: State containers defined as classes have a clear public API (constructor, methods). This makes them straightforward to instantiate, interact with, and assert against in unit tests, independent of the UI. While functional approaches to state management are popular, the class-based design in Blac provides a robust, type-safe, and extensible foundation for organizing and managing application state, particularly as complexity grows. @@ -33,10 +33,16 @@ While functional approaches to state management are popular, the class-based des Blac uses classes for its state containers (`Bloc` and `Cubit`) primarily to define state structure and business logic without immediate initialization. This design also inherently supports multiple instances of the same state container if needed. +### Initialization Strategy + +Blac employs lazy initialization to avoid circular dependency issues. The core `Blac` instance and its static methods are only created when first accessed, not during module loading. This prevents "Cannot access 'Blac' before initialization" errors that could occur with eager initialization patterns. + ```typescript // A Cubit definition (Blocs are similar) class MyCounterCubit extends Cubit { - constructor() { super(0); } + constructor() { + super(0); + } increment = () => this.emit(this.state + 1); } ``` @@ -57,8 +63,8 @@ function MyComponent() { When the last consumer of a shared `Bloc`/`Cubit` instance unregisters (e.g., its component unmounts), the instance is typically disposed of to free up resources. This behavior can be overridden: -- **`static keepAlive = true;`**: If set on the `Bloc`/`Cubit` class, the shared instance will persist in memory even if no components are currently listening to it. See [State Management Patterns](/learn/state-management-patterns#3-in-memory-persistence-keepalive) for more. -- **`static isolated = true;`**: If set, each `useBloc(MyIsolatedBloc)` call creates a new, independent instance of the `Bloc`/`Cubit`. This instance is then tied to the lifecycle of the component that created it and will be disposed of when that component unmounts (unless `keepAlive` is also true for that isolated class, which is a more advanced scenario). +- **`static keepAlive = true;`**: If set on the `Bloc`/`Cubit` class, the shared instance will persist in memory even if no components are currently listening to it. See [State Management Patterns](/learn/state-management-patterns#3-in-memory-persistence-keepalive) for more. +- **`static isolated = true;`**: If set, each `useBloc(MyIsolatedBloc)` call creates a new, independent instance of the `Bloc`/`Cubit`. This instance is then tied to the lifecycle of the component that created it and will be disposed of when that component unmounts (unless `keepAlive` is also true for that isolated class, which is a more advanced scenario). ```mermaid graph TD @@ -100,23 +106,29 @@ A `Bloc`/`Cubit` with `static isolated = true` behaves much like component-local ## Controlled Sharing Groups with Custom IDs -For more granular control over sharing, you can provide a custom `id` string in the `options` argument of the `useBloc` hook. Components that use the same `Bloc`/`Cubit` class **and the same `id`** will share an instance. This allows you to create specific sharing groups independent of the default class-name-based sharing or full isolation. +For more granular control over sharing, you can provide a custom `instanceId` string in the `options` argument of the `useBloc` hook. Components that use the same `Bloc`/`Cubit` class **and the same `instanceId`** will share an instance. This allows you to create specific sharing groups independent of the default class-name-based sharing or full isolation. ```tsx // ComponentA and ComponentB share one instance of ChatBloc for 'thread-alpha' function ComponentA() { - const [chatState, chatBloc] = useBloc(ChatBloc, { id: 'thread-alpha' }); + const [chatState, chatBloc] = useBloc(ChatBloc, { + instanceId: 'thread-alpha', + }); // ... } function ComponentB() { - const [chatState, chatBloc] = useBloc(ChatBloc, { id: 'thread-alpha' }); + const [chatState, chatBloc] = useBloc(ChatBloc, { + instanceId: 'thread-alpha', + }); // ... } // ComponentC uses a *different* instance of ChatBloc for 'thread-beta' function ComponentC() { - const [chatState, chatBloc] = useBloc(ChatBloc, { id: 'thread-beta' }); + const [chatState, chatBloc] = useBloc(ChatBloc, { + instanceId: 'thread-beta', + }); // ... } @@ -132,6 +144,7 @@ function AnotherComponent() { This is powerful for scenarios like managing state for multiple instances of a feature (e.g., different chat conversation windows, multiple editable data records on a page). #### Scenario 1: Default Sharing (No isolation, No custom ID) + ```mermaid graph TD CompA["Comp A: useBloc(MyBloc)"] --> SharedMyBloc["Shared MyBloc Instance"]; @@ -140,6 +153,7 @@ graph TD ``` #### Scenario 2: Isolated Instances (static isolated = true on MyIsolatedBloc) + ```mermaid graph TD CompA["Comp A: useBloc(MyIsolatedBloc)"] --> IsolatedA["MyIsolatedBloc for A"]; @@ -147,11 +161,12 @@ graph TD ``` #### Scenario 3: Custom ID Sharing (ChatBloc is not isolated by default) + ```mermaid graph TD - CompX["Comp X: useBloc(ChatBloc, {id: 'chat1'})"] --> Chat1["ChatBloc ('chat1')"]; - CompY["Comp Y: useBloc(ChatBloc, {id: 'chat1'})"] --> Chat1; - CompZ["Comp Z: useBloc(ChatBloc, {id: 'chat2'})"] --> Chat2["ChatBloc ('chat2')"]; + CompX["Comp X: useBloc(ChatBloc, {instanceId: 'chat1'})"] --> Chat1["ChatBloc ('chat1')"]; + CompY["Comp Y: useBloc(ChatBloc, {instanceId: 'chat1'})"] --> Chat1; + CompZ["Comp Z: useBloc(ChatBloc, {instanceId: 'chat2'})"] --> Chat2["ChatBloc ('chat2')"]; CompDef["Comp Default: useBloc(ChatBloc)"] --> DefaultChat["Default Shared ChatBloc (if any)"]; ``` @@ -165,9 +180,9 @@ While inspired by the BLoC pattern, Blac does not strictly enforce all its origi This typically leads to a layered architecture: -- **Presentation Layer (UI)**: Renders the UI based on state and forwards user events/intentions to the Business Logic Layer. -- **Business Logic Layer (State Containers)**: Contains `Bloc`s/`Cubit`s that hold application state, manage state mutations in response to events, and interact with the Data Layer. -- **Data Layer**: Handles data fetching, persistence, and communication with external services or APIs. +- **Presentation Layer (UI)**: Renders the UI based on state and forwards user events/intentions to the Business Logic Layer. +- **Business Logic Layer (State Containers)**: Contains `Bloc`s/`Cubit`s that hold application state, manage state mutations in response to events, and interact with the Data Layer. +- **Data Layer**: Handles data fetching, persistence, and communication with external services or APIs. ```mermaid flowchart LR @@ -181,6 +196,70 @@ flowchart LR This is what your users see and interact with. In Blac, React components subscribe to state from `Bloc`s or `Cubit`s using the `useBloc` hook and dispatch intentions by calling methods on the state container instance. +#### Proxy-Based Dependency Tracking + +BlaC employs an innovative proxy-based dependency tracking system in the presentation layer to optimize React re-renders automatically. When you use the `useBloc` hook, BlaC wraps both the state and bloc instance in ES6 Proxies that track which properties your component accesses during render. + +```typescript +// How proxy tracking works internally (simplified) +const stateProxy = new Proxy(actualState, { + get(target, property) { + // Track that this component accessed 'property' + trackDependency(componentId, property); + return target[property]; + }, +}); +``` + +This tracking happens transparently: + +```tsx +function UserCard() { + const [state, bloc] = useBloc(UserBloc); + + // BlaC tracks that this component only accesses 'name' and 'avatar' + return ( +
    + +

    {state.name}

    +
    + ); + // Component only re-renders when 'name' or 'avatar' change +} +``` + +**Benefits:** + +- **Automatic Optimization**: No need to manually specify dependencies +- **Fine-grained Updates**: Components only re-render when properties they use change +- **Dynamic Tracking**: Dependencies update automatically based on conditional rendering +- **Deep Object Support**: Works with nested properties (e.g., `state.user.profile.name`) + +**How It Works:** + +1. During render, the proxy intercepts all property access +2. BlaC builds a dependency map for each component +3. When state changes, BlaC checks if any tracked properties changed +4. Only components with changed dependencies re-render + +**Disabling Proxy Tracking:** +If needed, you can disable this globally: + +```typescript +import { Blac } from '@blac/core'; + +// Disable proxy tracking - all state changes trigger re-renders +Blac.setConfig({ proxyDependencyTracking: false }); +``` + +Or use manual dependencies per component: + +```tsx +const [state, bloc] = useBloc(UserBloc, { + dependencies: (bloc) => [bloc.state.name], // Manual control +}); +``` + ```tsx // Example: A React component in the Presentation Layer import { useBloc } from '@blac/react'; @@ -195,7 +274,9 @@ function UserProfileDisplay() { return (

    {userState.name}

    - +
    ); } @@ -231,4 +312,5 @@ class MyBloc extends Cubit { }; } ``` + Ensure the target `Bloc` (`OtherBloc` in this example) is indeed shared (not `static isolated = true`) and has been initialized (i.e., `useBloc(OtherBloc)` has been called elsewhere or it's `keepAlive`). diff --git a/apps/docs/learn/best-practices.md b/apps/docs/learn/best-practices.md index 00422ad8..60bb4525 100644 --- a/apps/docs/learn/best-practices.md +++ b/apps/docs/learn/best-practices.md @@ -12,7 +12,7 @@ Keep business logic in Blocs/Cubits and UI logic in components: // ❌ Don't do this - business logic in components function BadCounter() { const [count, setCount] = useState(0); - + const fetchCountFromAPI = async () => { try { const response = await fetch('/api/count'); @@ -22,7 +22,7 @@ function BadCounter() { console.error(error); } }; - + return (

    Count: {count}

    @@ -33,15 +33,15 @@ function BadCounter() { } // ✅ Do this - business logic in Cubit, UI in component -class CounterCubit extends Cubit<{ count: number, isLoading: boolean }> { +class CounterCubit extends Cubit<{ count: number; isLoading: boolean }> { constructor() { super({ count: 0, isLoading: false }); } - + increment = () => { this.patch({ count: this.state.count + 1 }); }; - + fetchCount = async () => { try { this.patch({ isLoading: true }); @@ -57,7 +57,7 @@ class CounterCubit extends Cubit<{ count: number, isLoading: boolean }> { function GoodCounter() { const [state, bloc] = useBloc(CounterCubit); - + return (

    Count: {state.count}

    @@ -95,16 +95,18 @@ Design your state to be serializable to make debugging easier: // ❌ Don't do this - non-serializable state class BadUserCubit extends Cubit<{ user: { - name: string, - dateJoined: Date, // Date objects aren't serializable - logout: () => void // Functions aren't serializable - } -}> { /* ... */ } + name: string; + dateJoined: Date; // Date objects aren't serializable + logout: () => void; // Functions aren't serializable + }; +}> { + /* ... */ +} // ✅ Do this - serializable state class GoodUserCubit extends Cubit<{ - name: string, - dateJoinedTimestamp: number, // Use timestamps instead of Date objects + name: string; + dateJoinedTimestamp: number; // Use timestamps instead of Date objects }> { logout = () => { // Keep methods in the cubit, not in the state @@ -121,32 +123,32 @@ Flatten state to reduce the number of re-renders, and to make it easier to patch // ❌ Don't do this - nested state class BadUserCubit extends Cubit<{ user: { - name: string, - email: string, - }, - isLoading: boolean -}> { + name: string; + email: string; + }; + isLoading: boolean; +}> { // ... updateName = (name: string) => { - this.patch({ + this.patch({ user: { ...this.state.user, - name - } + name, + }, }); - } -} + }; +} // ✅ Do this - flattened state class GoodUserCubit extends Cubit<{ - name: string, - email: string, - isLoading: boolean -}> { + name: string; + email: string; + isLoading: boolean; +}> { // ... updateName = (name: string) => { this.patch({ name }); - } + }; } ``` @@ -156,11 +158,72 @@ class GoodUserCubit extends Cubit<{ Components should focus on rendering state and dispatching events: +### 2. Optimize Re-renders with Proxy Tracking + +BlaC's automatic proxy dependency tracking optimizes re-renders by default. Understanding how to work with it effectively is crucial: + +```tsx +// ✅ Do this - leverage automatic tracking +function UserCard() { + const [state, bloc] = useBloc(UserBloc); + + // Only re-renders when name or avatar change + return ( +
    + +

    {state.name}

    +
    + ); +} + +// ✅ Do this - use manual dependencies when needed +function UserStats() { + const [state, bloc] = useBloc(UserBloc, { + // Explicitly control dependencies + dependencies: (bloc) => [bloc.state.postCount, bloc.state.followerCount], + }); + + return ( +
    +

    Posts: {state.postCount}

    +

    Followers: {state.followerCount}

    +
    + ); +} +``` + +### 3. When to Disable Proxy Tracking + +While proxy tracking is beneficial in most cases, there are scenarios where you might want to disable it: + +```tsx +// Disable globally when: +// 1. Debugging re-render issues +// 2. Working with third-party libraries that don't play well with proxies +// 3. Preferring simpler mental model over optimization + +Blac.setConfig({ proxyDependencyTracking: false }); +``` + +**Consider disabling proxy tracking when:** + +- Your state objects are small and simple +- You're experiencing compatibility issues with dev tools +- You prefer explicit control over re-renders +- You're migrating from another state management solution + +**Keep proxy tracking enabled when:** + +- Working with large state objects +- Building performance-critical applications +- You have components that only use a subset of state +- You want automatic optimization without manual work + ```tsx // ✅ Do this - simple component that renders state and dispatches events function UserProfile() { const [state, bloc] = useBloc(UserBloc); - + return (
    {state.isLoading ? ( @@ -186,14 +249,14 @@ Errors should be handled in the Bloc/Cubit and reflected in the state: ```tsx class UserBloc extends Cubit<{ - user: User | null, - isLoading: boolean, - error: string | null + user: User | null; + isLoading: boolean; + error: string | null; }> { constructor() { super({ user: null, isLoading: false, error: null }); } - + fetchUser = async (id: string) => { try { this.patch({ isLoading: true, error: null }); @@ -202,7 +265,7 @@ class UserBloc extends Cubit<{ } catch (error) { this.patch({ isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch user' + error: error instanceof Error ? error.message : 'Failed to fetch user', }); } }; @@ -217,16 +280,24 @@ Each Bloc/Cubit should manage a single aspect of your application state: ```tsx // ✅ Do this - separate blocs for different concerns -class AuthBloc extends Cubit { /* ... */ } -class ProfileBloc extends Cubit { /* ... */ } -class SettingsBloc extends Cubit { /* ... */ } +class AuthBloc extends Cubit { + /* ... */ +} +class ProfileBloc extends Cubit { + /* ... */ +} +class SettingsBloc extends Cubit { + /* ... */ +} // ❌ Don't do this - one bloc handling too many concerns class MegaBloc extends Cubit<{ - auth: AuthState, - profile: ProfileState, - settings: SettingsState -}> { /* ... */ } + auth: AuthState; + profile: ProfileState; + settings: SettingsState; +}> { + /* ... */ +} ``` ### 2. Choose the Right Instance Pattern @@ -248,7 +319,7 @@ Test your Blocs/Cubits independently of components: test('CounterCubit increments count', async () => { const cubit = new CounterCubit(); expect(cubit.state.count).toBe(0); - + cubit.increment(); expect(cubit.state.count).toBe(1); }); @@ -256,15 +327,43 @@ test('CounterCubit increments count', async () => { test('CounterCubit fetches count from API', async () => { // Mock API global.fetch = jest.fn().mockResolvedValue({ - json: jest.fn().mockResolvedValue({ count: 42 }) + json: jest.fn().mockResolvedValue({ count: 42 }), }); - + const cubit = new CounterCubit(); await cubit.fetchCount(); - + expect(cubit.state.count).toBe(42); expect(cubit.state.isLoading).toBe(false); }); ``` -By following these best practices, you'll create more maintainable, testable, and efficient applications with Blac. \ No newline at end of file +### 2. Test with Different Configurations + +When testing components that rely on proxy tracking behavior, consider testing with different configurations: + +```tsx +import { Blac } from '@blac/core'; + +describe('UserComponent', () => { + const originalConfig = { ...Blac.config }; + + afterEach(() => { + // Reset configuration after each test + Blac.setConfig(originalConfig); + Blac.resetInstance(); + }); + + test('renders efficiently with proxy tracking', () => { + Blac.setConfig({ proxyDependencyTracking: true }); + // Test that component only re-renders when accessed properties change + }); + + test('handles all updates without proxy tracking', () => { + Blac.setConfig({ proxyDependencyTracking: false }); + // Test that component re-renders on any state change + }); +}); +``` + +By following these best practices, you'll create more maintainable, testable, and efficient applications with Blac. diff --git a/apps/docs/learn/blac-pattern.md b/apps/docs/learn/blac-pattern.md index b2a7719f..7d603159 100644 --- a/apps/docs/learn/blac-pattern.md +++ b/apps/docs/learn/blac-pattern.md @@ -22,10 +22,10 @@ interface CounterState { **Best Practices for State Design:** -- Keep state serializable (avoid complex class instances, functions, etc., directly in the state if persistence or straightforward debugging is a goal). -- Prefer flatter state structures where possible, but organize logically. -- Explicitly include UI-related states like `isLoading`, `error`, etc. -- Utilize TypeScript interfaces or types for robust type safety. +- Keep state serializable (avoid complex class instances, functions, etc., directly in the state if persistence or straightforward debugging is a goal). +- Prefer flatter state structures where possible, but organize logically. +- Explicitly include UI-related states like `isLoading`, `error`, etc. +- Utilize TypeScript interfaces or types for robust type safety. See more in [Best Practices for State Container Design](/learn/best-practices#state-container-design). @@ -35,50 +35,60 @@ State containers are classes that hold the current `State` and contain the busin Blac offers two main types of state containers: -- **`Cubit`**: A simpler container that exposes methods to directly `emit` new states or `patch` the existing state. +- **`Cubit`**: A simpler container that exposes methods to directly `emit` new states or `patch` the existing state. - ```typescript - // Example of a Cubit state container - import { Cubit } from '@blac/core'; + ```typescript + // Example of a Cubit state container + import { Cubit } from '@blac/core'; - class CounterCubit extends Cubit { - constructor() { - super({ count: 0, isLoading: false, error: null }); - } - - increment = () => { - this.patch({ count: this.state.count + 1, lastUpdated: Date.now() }); - }; - - decrement = () => { - this.patch({ count: this.state.count - 1, lastUpdated: Date.now() }); - }; - - reset = () => { - // 'emit' can be used to completely replace the state - this.emit({ count: 0, isLoading: false, error: null, lastUpdated: Date.now() }); - }; - - // Example async operation - fetchCount = async () => { - this.patch({ isLoading: true, error: null }); - try { - // const response = await api.fetchCount(); // Replace with actual API call - await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call - const fetchedCount = Math.floor(Math.random() * 100); - this.patch({ count: fetchedCount, isLoading: false, lastUpdated: Date.now() }); - } catch (err) { - this.patch({ - isLoading: false, - error: err instanceof Error ? err.message : 'An unknown error occurred', - lastUpdated: Date.now(), - }); - } - }; + class CounterCubit extends Cubit { + constructor() { + super({ count: 0, isLoading: false, error: null }); } - ``` -- **`Bloc`**: A more structured container that processes `Action` objects through a `reducer` function to produce new `State`. This is ideal for more complex state transitions. + increment = () => { + this.patch({ count: this.state.count + 1, lastUpdated: Date.now() }); + }; + + decrement = () => { + this.patch({ count: this.state.count - 1, lastUpdated: Date.now() }); + }; + + reset = () => { + // 'emit' can be used to completely replace the state + this.emit({ + count: 0, + isLoading: false, + error: null, + lastUpdated: Date.now(), + }); + }; + + // Example async operation + fetchCount = async () => { + this.patch({ isLoading: true, error: null }); + try { + // const response = await api.fetchCount(); // Replace with actual API call + await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate API call + const fetchedCount = Math.floor(Math.random() * 100); + this.patch({ + count: fetchedCount, + isLoading: false, + lastUpdated: Date.now(), + }); + } catch (err) { + this.patch({ + isLoading: false, + error: + err instanceof Error ? err.message : 'An unknown error occurred', + lastUpdated: Date.now(), + }); + } + }; + } + ``` + +- **`Bloc`**: A more structured container that processes instances of event _classes_ through registered _handler functions_ to produce new `State`. Event instances are dispatched via `this.add(new EventType())`, and handlers are registered with `this.on(EventType, handler)`. This is ideal for more complex, type-safe state transitions. Details on these are in the [Core Classes API](/api/core-classes) and [Core Concepts](/learn/core-concepts). @@ -97,13 +107,26 @@ function CounterDisplay() { return (
    {state.isLoading &&

    Loading...

    } - {!state.isLoading && state.error &&

    Error: {state.error}

    } + {!state.isLoading && state.error && ( +

    Error: {state.error}

    + )} {!state.isLoading && !state.error &&

    Count: {state.count}

    } -

    Last updated: {state.lastUpdated ? new Date(state.lastUpdated).toLocaleTimeString() : 'N/A'}

    +

    + Last updated:{' '} + {state.lastUpdated + ? new Date(state.lastUpdated).toLocaleTimeString() + : 'N/A'} +

    - - - + + + @@ -125,8 +148,9 @@ The Blac pattern enforces a strict unidirectional data flow, making state change │ (Cubit / Bloc) │ └────────┬──────────┘ │ -(State updates via │ State (Immutable) - internal logic or reducer)│ +State updates via internal │ State (Immutable) +logic (Cubit) or event │ +handlers (Bloc) │ ▼ ┌────────┴──────────┐ │ UI Component │ @@ -141,7 +165,7 @@ The Blac pattern enforces a strict unidirectional data flow, making state change 1. **UI Components** render based on the current `State` from a `Cubit` or `Bloc`. 2. **User interactions** (or other events) in the UI trigger method calls on the `Cubit`/`Bloc` instance. -3. The **`Cubit`/`Bloc`** contains business logic. It processes the method call (or `Action` in a `Bloc`), produces a new `State`. +3. The **`Cubit`/`Bloc`** contains business logic. A `Cubit` processes the method call directly. A `Bloc` processes dispatched event instances through its registered event handlers. It produces a new `State`. 4. The **State Container** notifies its listeners (including the `useBloc` hook) that its state has changed. 5. The **UI Component** re-renders with the new `State`. @@ -159,22 +183,19 @@ This cycle ensures that changes are easy to follow and debug. The Blac pattern is beneficial for: -- Components or features with non-trivial business logic. -- Managing asynchronous operations, including loading and error states. -- Sharing state across multiple components or sections of your application. -- Applications requiring a robust, predictable, and traceable state management architecture. - -## How to Choose: `Cubit` vs. `Bloc` vs. `createBloc().setState()` - -- Use **`Cubit`** when: - - State logic is relatively simple. - - You prefer updating state via direct method calls (`emit`, `patch`). - - Formal `Action` objects and `reducer`s feel like overkill. -- Use **`Bloc`** when: - - State transitions are complex and benefit from explicit `Action`s. - - You want to leverage the traditional reducer pattern for better traceability of events leading to state changes. -- Use **`createBloc().setState()` style** when: - - You want `Cubit`-like simplicity (direct state changes via methods). - - You prefer a `this.setState()` API similar to React class components for its conciseness (often handles partial state updates on objects conveniently). - -Refer to the [Core Classes API](/api/core-classes) and [Key Methods API](/api/key-methods) for more implementation details. \ No newline at end of file +- Components or features with non-trivial business logic. +- Managing asynchronous operations, including loading and error states. +- Sharing state across multiple components or sections of your application. +- Applications requiring a robust, predictable, and traceable state management architecture. + +## How to Choose: `Cubit` vs. `Bloc` + +- Use **`Cubit`** when: + - State logic is relatively simple. + - You prefer updating state via direct method calls (`emit`, `patch`). + - Formal event classes and handlers feel like overkill. +- Use **`Bloc`** (with its `this.on(EventType, handler)` and `this.add(new EventType())` pattern) when: + - State transitions are complex and benefit from distinct, type-safe event classes and dedicated handlers. + - You want a clear, event-driven architecture for managing state changes and side effects, enhancing traceability and maintainability. + +Refer to the [Core Classes API](/api/core-classes) and [Key Methods API](/api/key-methods) for more implementation details. diff --git a/apps/docs/learn/core-concepts.md b/apps/docs/learn/core-concepts.md index 221d38f5..baf909a1 100644 --- a/apps/docs/learn/core-concepts.md +++ b/apps/docs/learn/core-concepts.md @@ -20,19 +20,19 @@ Blac offers two primary types of state containers, both built upon a common `Blo `BlocBase` is the abstract foundation for all state containers. It provides core functionalities like: -- Internal state management and update mechanisms (`_state`, `_pushState`). -- An observer notification system (`_observer`). -- Lifecycle event dispatching through the main `Blac` instance. -- Instance management (ID, isolation, keep-alive status). +- Internal state management and update mechanisms (`_state`, `_pushState`). +- An observer notification system (`_observer`). +- Lifecycle event dispatching through the main `Blac` instance. +- Instance management (ID, isolation, keep-alive status). -You typically won't extend `BlocBase` directly. Instead, you'll use `Cubit` or `Bloc` (or `createBloc`). Refer to the [Core Classes API](/api/core-classes) for deeper details. +You typically won't extend `BlocBase` directly. Instead, you'll use `Cubit` or `Bloc`. Refer to the [Core Classes API](/api/core-classes) for deeper details. -### `Cubit` +### `Cubit` A `Cubit` is the simpler of the two. It exposes methods that directly cause state changes by calling `this.emit()` or `this.patch()`. This approach is often preferred for straightforward state management where the logic for state changes can be encapsulated within direct method calls, similar to how state is managed in libraries like Zustand. -- `emit(newState: State)`: Replaces the entire current state with `newState`. -- `patch(partialState: Partial)`: Merges `partialState` with the current state (if the state is an object). +- `emit(newState: State)`: Replaces the entire current state with `newState`. +- `patch(partialState: Partial)`: Merges `partialState` with the current state (if the state is an object). ```tsx import { Cubit } from '@blac/core'; @@ -59,62 +59,67 @@ class CounterCubit extends Cubit { reset = () => { this.emit({ count: 0, lastAction: 'reset' }); - } + }; } ``` + Cubits are excellent for straightforward state management. See [Cubit API details](/api/core-classes#cubit-s-p). -### `Bloc` +### `Bloc` -A `Bloc` is more structured and suited for complex state logic. It processes `Action`s (events) which are dispatched to it via its `add(action)` method. These actions are then handled by a `reducer` function. The `reducer` is a pure function that takes the current state and the dispatched action, and returns a new state. This event-driven update cycle, where state transitions are explicit and decoupled, is similar to the reducer pattern found in Redux. +A `Bloc` is more structured and suited for complex state logic. It processes instances of event _classes_ which are dispatched to it via its `add(eventInstance)` method. These events are then handled by specific handler functions registered for each event class using `this.on(EventClass, handler)`. This event-driven update cycle, where state transitions are explicit and type-safe, allows for clear and decoupled business logic. -- `add(action: Action)`: Dispatches an action to the `reducer`. -- `reducer(action: Action, currentState: State): State`: A pure function that defines how the state changes in response to actions. +- `on(EventClass, handler)`: Registers a handler function for a specific event class. The handler receives the event instance and an `emit` function to produce new state. +- `add(eventInstance: Event)`: Dispatches an instance of an event class. The `Bloc` looks up the registered handler based on the event instance's constructor. ```tsx import { Bloc } from '@blac/core'; -// 1. Define State and Actions +// 1. Define State and Event Classes interface CounterState { count: number; } -type CounterAction = - | { type: 'INCREMENT' } - | { type: 'DECREMENT' } - | { type: 'RESET' }; +class IncrementEvent { + constructor(public readonly value: number = 1) {} +} +class DecrementEvent {} +class ResetEvent {} + +// Optional: Union type for all events the Bloc handles +type CounterBlocEvents = IncrementEvent | DecrementEvent | ResetEvent; // 2. Create the Bloc -class CounterBloc extends Bloc { +class CounterBloc extends Bloc { constructor() { super({ count: 0 }); // Initial state - } - // 3. Implement the reducer - reducer = (action: CounterAction, state: CounterState): CounterState => { - switch (action.type) { - case 'INCREMENT': - return { ...state, count: state.count + 1 }; - case 'DECREMENT': - return { ...state, count: state.count - 1 }; - case 'RESET': - return { ...state, count: 0 }; - default: - return state; - } - }; + // 3. Register event handlers + this.on(IncrementEvent, (event, emit) => { + emit({ ...this.state, count: this.state.count + event.value }); + }); + + this.on(DecrementEvent, (_event, emit) => { + emit({ ...this.state, count: this.state.count - 1 }); + }); - // 4. (Optional) Helper methods to dispatch actions - increment = () => this.add({ type: 'INCREMENT' }); - decrement = () => this.add({ type: 'DECREMENT' }); - reset = () => this.add({ type: 'RESET' }); + this.on(ResetEvent, (_event, emit) => { + emit({ ...this.state, count: 0 }); + }); + } + + // 4. (Optional) Helper methods to dispatch event instances + increment = (value?: number) => this.add(new IncrementEvent(value)); + decrement = () => this.add(new DecrementEvent()); + reset = () => this.add(new ResetEvent()); } ``` -Blocs are ideal for complex state logic where transitions need to be more explicit and benefit from an event-driven architecture. See [Bloc API details](/api/core-classes#bloc-s-a-p). + +Blocs are ideal for complex state logic where transitions need to be more explicit, type-safe, and benefit from an event-driven architecture. See [Bloc API details](/api/core-classes#bloc-s-e-p). ## State Updates & Reactivity -When a `Cubit` calls `emit`/`patch`, or a `Bloc`'s reducer produces a new state after an `add` call, the state container updates its internal state and notifies its observers. +When a `Cubit` calls `emit`/`patch`, or a `Bloc`'s registered event handler emits a new state after an `add` call, the state container updates its internal state and notifies its observers. In React applications, the `useBloc` hook subscribes your components to these updates, triggering re-renders efficiently when relevant parts of the state change. @@ -133,7 +138,7 @@ function CounterComponent() { return (

    Count: {state.count}

    - {/* Call methods directly on the instance */} + {/* Call methods directly on the instance */}
    ); @@ -142,9 +147,9 @@ function CounterComponent() { Key features of `useBloc`: -- **Automatic Property Tracking**: Efficiently re-renders components only when the state properties they *actually access* change. -- **Instance Management**: Handles creation and retrieval of shared or isolated state container instances. -- **Props for Blocs**: Allows passing props to your `Bloc` or `Cubit` constructors. +- **Automatic Property Tracking**: Efficiently re-renders components only when the state properties they _actually access_ change. +- **Instance Management**: Handles creation and retrieval of shared or isolated state container instances. +- **Props for Blocs**: Allows passing props to your `Bloc` or `Cubit` constructors. For more details, see the [React Hooks API](/api/react-hooks). @@ -153,14 +158,14 @@ For more details, see the [React Hooks API](/api/react-hooks). Blac, through its central `Blac` instance, offers flexible ways to manage your `Bloc` or `Cubit` instances: 1. **Shared State (Default for non-isolated Blocs)**: - When a `Bloc` or `Cubit` is *not* marked as `static isolated = true;`, instances are typically shared. + When a `Bloc` or `Cubit` is _not_ marked as `static isolated = true;`, instances are typically shared. Components requesting such a `Bloc`/`Cubit` (e.g., via `useBloc(MyBloc)`) will receive the same instance if one already exists with a matching ID. - By default, Blac uses the class name as the ID (e.g., `"MyBloc"`). You can also provide a specific `id` in the `useBloc` options (e.g., `useBloc(MyBloc, { id: 'customSharedId' })`) to share an instance under that custom ID. If no instance exists for the determined ID, a new one is created and registered for future sharing. + By default, Blac uses the class name as the ID (e.g., `"MyBloc"`). You can also provide a specific `instanceId` in the `useBloc` options (e.g., `useBloc(MyBloc, { instanceId: 'customSharedId' })`) to share an instance under that custom ID. If no instance exists for the determined ID, a new one is created and registered for future sharing. 2. **Isolated State**: To ensure a component gets its own unique instance of a `Bloc` or `Cubit`, you can either: - * Set `static isolated = true;` on your `Bloc`/`Cubit` class. When using `useBloc(MyIsolatedBloc)`, Blac will attempt to find an existing isolated instance using the class name as the default ID. If you need multiple, distinct isolated instances of the *same class* for different components or use cases, you *must* provide a unique `id` via `useBloc` options (e.g., `useBloc(MyIsolatedBloc, { id: 'uniqueInstance1' })`). Blac's `findIsolatedBlocInstance` method will use this ID to retrieve the specific instance. If no isolated instance with that ID is found, a new one is created and registered under that ID in a separate registry for isolated blocs. - * Even if a Bloc is not `static isolated`, you can achieve a similar effect by always providing a guaranteed unique `id` string through `useBloc` options. The `Blac` instance will then manage it as a distinct, non-isolated instance under that unique ID. + - Set `static isolated = true;` on your `Bloc`/`Cubit` class. When using `useBloc(MyIsolatedBloc)`, Blac will attempt to find an existing isolated instance using the class name as the default ID. If you need multiple, distinct isolated instances of the _same class_ for different components or use cases, you _must_ provide a unique `instanceId` via `useBloc` options (e.g., `useBloc(MyIsolatedBloc, { instanceId: 'uniqueInstance1' })`). Blac's `findIsolatedBlocInstance` method will use this ID to retrieve the specific instance. If no isolated instance with that ID is found, a new one is created and registered under that ID in a separate registry for isolated blocs. + - Even if a Bloc is not `static isolated`, you can achieve a similar effect by always providing a guaranteed unique `instanceId` string through `useBloc` options. The `Blac` instance will then manage it as a distinct, non-isolated instance under that unique ID. 3. **In-Memory Persistence (`keepAlive`)**: You can prevent a `Bloc`/`Cubit` (whether shared or isolated, though more common for shared) from being automatically disposed when no components are actively listening to it. Set `static keepAlive = true;` on the `Bloc`/`Cubit` class. The instance will remain in memory until manually disposed or the `Blac` instance is reset. @@ -179,6 +184,6 @@ Learn more in the [State Management Patterns](/learn/state-management-patterns) With these core concepts in mind, you are ready to: -- Delve into the [Blac Pattern](/learn/blac-pattern) for a deeper architectural understanding. -- Review [Best Practices](/learn/best-practices) for writing effective and maintainable Blac code. -- Consult the full [API Reference](/api/core-classes) for detailed documentation on all classes and methods. \ No newline at end of file +- Delve into the [Blac Pattern](/learn/blac-pattern) for a deeper architectural understanding. +- Review [Best Practices](/learn/best-practices) for writing effective and maintainable Blac code. +- Consult the full [API Reference](/api/core-classes) for detailed documentation on all classes and methods. diff --git a/apps/docs/learn/getting-started.md b/apps/docs/learn/getting-started.md index 7706fc91..c3f0d5fa 100644 --- a/apps/docs/learn/getting-started.md +++ b/apps/docs/learn/getting-started.md @@ -1,11 +1,15 @@ # Getting Started +:::tip Recommended Reading +This page contains legacy content. For the most up-to-date getting started guide, please see the [Getting Started](/getting-started/installation) section. +::: + Welcome to Blac! This guide will walk you through setting up Blac and creating your first reactive state container. Blac is a collection of packages designed for robust state management: -- `@blac/core`: The core engine providing `Cubit`, `Bloc`, `BlocBase`, and the underlying instance management logic. -- `@blac/react`: The React integration, offering hooks like `useBloc` to connect your components to Blac state containers. +- `@blac/core`: The core engine providing `Cubit`, `Bloc`, `BlocBase`, and the underlying instance management logic. +- `@blac/react`: The React integration, offering hooks like `useBloc` to connect your components to Blac state containers. ## Installation @@ -73,16 +77,75 @@ export default CounterDisplay; That's it! You've created a simple counter using a Blac `Cubit`. +## Configuration (Optional) + +BlaC provides global configuration options to customize its behavior. The most common configuration is controlling proxy dependency tracking: + +```tsx +import { Blac } from '@blac/core'; + +// Configure BlaC before your app starts +Blac.setConfig({ + // Control automatic re-render optimization (default: true) + proxyDependencyTracking: true, +}); +``` + +By default, BlaC uses proxy-based dependency tracking to automatically optimize re-renders. Components only re-render when the specific state properties they access change. You can disable this globally if needed: + +```tsx +// Disable automatic optimization - components re-render on any state change +Blac.setConfig({ proxyDependencyTracking: false }); +``` + +For more details, see the [Configuration](/api/configuration) documentation. + +## Common Issues + +### "Cannot access 'Blac' before initialization" Error + +If you encounter this error, it's likely due to circular dependencies in your imports. This has been resolved in v2.0.0-rc-3+ with lazy initialization. Make sure you're using the latest version: + +```bash +pnpm update @blac/core @blac/react +``` + +If the issue persists, avoid immediately accessing static Blac properties at module level. Instead, access them inside functions or component lifecycle: + +```tsx +// ❌ Avoid this at module level +Blac.enableLog = true; + +// ✅ Do this instead +useEffect(() => { + Blac.enableLog = true; +}, []); +``` + +### TypeScript Type Inference Issues + +If you're experiencing TypeScript errors with `useBloc` not properly inferring your Cubit/Bloc types, ensure you're using v2.0.0-rc-3+ which includes improved type constraints: + +```tsx +// This should now work correctly with proper type inference +const [state, cubit] = useBloc(CounterCubit, { + instanceId: 'unique-id', + staticProps: { initialCount: 0 }, +}); +// state.count is properly typed as number +// cubit.increment is properly typed as () => void +``` + ### How It Works 1. **`CounterCubit`**: Extends `Cubit` from `@blac/core`. - * Defines its state structure (`CounterState`). - * Sets an initial state in its constructor. - * Provides methods (`increment`, `decrement`, `reset`) that call `this.emit()` with a new state object. `emit` replaces the entire state. + - Defines its state structure (`CounterState`). + - Sets an initial state in its constructor. + - Provides methods (`increment`, `decrement`, `reset`) that call `this.emit()` with a new state object. `emit` replaces the entire state. 2. **`CounterDisplay` Component**: Uses the `useBloc` hook from `@blac/react`. - * `useBloc(CounterCubit)` gets (or creates) an instance of `CounterCubit`. - * It returns an array `[state, counterCubit]`. The `state` object is a reactive proxy that tracks property access. - * The component re-renders automatically and efficiently when `state.count` changes due to a `counterCubit` method call. + - `useBloc(CounterCubit)` gets (or creates) an instance of `CounterCubit`. + - It returns an array `[state, counterCubit]`. The `state` object is a reactive proxy that tracks property access. + - The component re-renders automatically and efficiently when `state.count` changes due to a `counterCubit` method call. ### Important: Arrow Functions for Methods @@ -107,6 +170,7 @@ Cubits can easily handle asynchronous operations. Let's extend our counter to fe `Cubit` provides a `patch()` method for updating only specific parts of an object state. ::: code-group + ```tsx [CounterCubit.ts] import { Cubit } from '@blac/core'; @@ -134,7 +198,7 @@ export class AsyncCounterCubit extends Cubit { this.patch({ isLoading: true, error: null }); try { // Simulate API call (replace with your actual fetch logic) - await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate delay const randomNumber = Math.floor(Math.random() * 100); // const response = await fetch('https://api.example.com/random-number'); // if (!response.ok) throw new Error('Network response was not ok'); @@ -147,7 +211,10 @@ export class AsyncCounterCubit extends Cubit { } catch (error) { this.patch({ isLoading: false, - error: error instanceof Error ? error.message : 'Failed to fetch random count', + error: + error instanceof Error + ? error.message + : 'Failed to fetch random count', }); } }; @@ -178,12 +245,14 @@ function AsyncCounterDisplay() { export default AsyncCounterDisplay; ``` + ::: Notice in the async example: -- The state (`AsyncCounterState`) now includes `isLoading` and `error` fields. -- `this.patch()` is used to update parts of the state without needing to spread the rest (`{ ...this.state, isLoading: true }`). -- Error handling is included within the `fetchRandomCount` method. -- The UI (`AsyncCounterDisplay`) conditionally renders loading/error messages and disables buttons during loading. -This covers the basics of getting started with Blac using `Cubit`. Explore further sections to learn about the more advanced `Bloc` class (with events and reducers), advanced patterns, and API details. \ No newline at end of file +- The state (`AsyncCounterState`) now includes `isLoading` and `error` fields. +- `this.patch()` is used to update parts of the state without needing to spread the rest (`{ ...this.state, isLoading: true }`). +- Error handling is included within the `fetchRandomCount` method. +- The UI (`AsyncCounterDisplay`) conditionally renders loading/error messages and disables buttons during loading. + +This covers the basics of getting started with Blac using `Cubit`. Explore further sections to learn about the more advanced `Bloc` class (which uses an event-handler pattern: `this.on(EventType, handler)` and `this.add(new EventType())`), advanced patterns, and API details. diff --git a/apps/docs/learn/introduction.md b/apps/docs/learn/introduction.md index 466ce717..c67d1107 100644 --- a/apps/docs/learn/introduction.md +++ b/apps/docs/learn/introduction.md @@ -1,5 +1,9 @@ # Introduction to Blac +:::tip Recommended Reading +This page contains legacy content. For the most up-to-date introduction, please see the main [Introduction](/introduction) page. +::: + Welcome to Blac, a modern state management library designed to bring simplicity, power, and predictability to your React projects! Blac aims to reduce the mental overhead of state management by cleanly separating business logic from your UI components. ## What is Blac? @@ -7,44 +11,45 @@ Welcome to Blac, a modern state management library designed to bring simplicity, Blac is a state management solution built with **TypeScript first**, offering a lightweight yet flexible approach. It draws inspiration from established patterns while providing an intuitive API to minimize boilerplate. It consists of two main packages: -- `@blac/core`: The foundational library providing the core Blac/Bloc logic, instance management, and a plugin system. -- `@blac/react`: The React integration layer, offering custom hooks and utilities to seamlessly connect Blac with your React components. + +- `@blac/core`: The foundational library providing the core Blac/Bloc logic, instance management, and a plugin system. +- `@blac/react`: The React integration layer, offering custom hooks and utilities to seamlessly connect Blac with your React components. In Blac's architecture, state becomes a well-defined side effect of your business logic, and the UI becomes a reactive reflection of that state. ## Key Features of Blac -- 💡 **Simple & Intuitive API**: Get started quickly with familiar concepts and less boilerplate. -- 🧠 **Smart Instance Management**: - - Automatic creation, sharing (default for non-isolated Blocs, keyed by class name or custom ID), and disposal of `Bloc`/`Cubit` instances, orchestrated by the central `Blac` class. - - Support for `static isolated = true` on Blocs/Cubits or providing unique IDs for component-specific/distinct instances. - - `Bloc`s/`Cubit`s are kept alive as long as they have active listeners/consumers, or if explicitly marked with `static keepAlive = true`. -- �� **TypeScript First**: Full type safety out-of-the-box, enabling robust applications and great autocompletion. -- 🧩 **Extensible via Plugins & Addons**: - - **Plugins**: Extend Blac's core functionality by hooking into lifecycle events (e.g., for custom logging). - - **Addons**: Enhance individual `Bloc` capabilities (e.g., state persistence with the built-in `Persist` addon). -- 🚀 **Performance Focused**: - - Minimal dependencies for a small bundle size. - - Efficient state updates and re-renders in React. -- 🧱 **Flexible Architecture**: Adapts to various React project structures and complexities. +- 💡 **Simple & Intuitive API**: Get started quickly with familiar concepts and less boilerplate. +- 🧠 **Smart Instance Management**: + - Automatic creation, sharing (default for non-isolated Blocs, keyed by class name or custom ID), and disposal of `Bloc`/`Cubit` instances, orchestrated by the central `Blac` class. + - Support for `static isolated = true` on Blocs/Cubits or providing unique IDs for component-specific/distinct instances. + - `Bloc`s/`Cubit`s are kept alive as long as they have active listeners/consumers, or if explicitly marked with `static keepAlive = true`. +- �� **TypeScript First**: Full type safety out-of-the-box, enabling robust applications and great autocompletion. +- 🧩 **Extensible via Plugins & Addons**: + - **Plugins**: Extend Blac's core functionality by hooking into lifecycle events (e.g., for custom logging). + - **Addons**: Enhance individual `Bloc` capabilities (e.g., state persistence with the built-in `Persist` addon). +- 🚀 **Performance Focused**: + - Minimal dependencies for a small bundle size. + - Efficient state updates and re-renders in React. +- 🧱 **Flexible Architecture**: Adapts to various React project structures and complexities. ## Documentation Structure -This documentation is organized to help you learn Blac 효과적으로 (effectively): +This documentation is organized to help you learn Blac effectively: -- **Learn**: Foundational concepts, patterns, and guides. - - [Getting Started](/learn/getting-started): Your first steps with Blac. - - [Core Concepts](/learn/core-concepts): Understand the fundamental ideas and architecture. - - [The Blac Pattern](/learn/blac-pattern): Dive into the unidirectional data flow and principles. - - [State Management Patterns](/learn/state-management-patterns): Explore different approaches to sharing state. - - [Architecture](/learn/architecture): A deeper look at Blac's internal structure. - - [Best Practices](/learn/best-practices): Recommended techniques for building robust applications. +- **Learn**: Foundational concepts, patterns, and guides. + - [Getting Started](/learn/getting-started): Your first steps with Blac. + - [Core Concepts](/learn/core-concepts): Understand the fundamental ideas and architecture. + - [The Blac Pattern](/learn/blac-pattern): Dive into the unidirectional data flow and principles. + - [State Management Patterns](/learn/state-management-patterns): Explore different approaches to sharing state. + - [Architecture](/learn/architecture): A deeper look at Blac's internal structure. + - [Best Practices](/learn/best-practices): Recommended techniques for building robust applications. -- **API Reference**: Detailed information about Blac's classes, hooks, and methods. - - Core (`@blac/core`): - - [Core Classes (BlocBase, Bloc, Cubit)](/api/core-classes): Detailed references for the main state containers. - - [Key Methods](/api/key-methods): Essential methods for creating and managing state. - - React (`@blac/react`): - - [React Hooks (useBloc, useValue, etc.)](/api/react-hooks): Learn how to use Blac with your React components. +- **API Reference**: Detailed information about Blac's classes, hooks, and methods. + - Core (`@blac/core`): + - [Core Classes (BlocBase, Bloc, Cubit)](/api/core-classes): Detailed references for the main state containers. + - [Key Methods](/api/key-methods): Essential methods for creating and managing state. + - React (`@blac/react`): + - [React Hooks (useBloc, useExternalBlocStore)](/api/react-hooks): Learn how to use Blac with your React components. -Ready to begin? Jump into the [Getting Started](/learn/getting-started) guide! \ No newline at end of file +Ready to begin? Jump into the [Getting Started](/learn/getting-started) guide! diff --git a/apps/docs/learn/state-management-patterns.md b/apps/docs/learn/state-management-patterns.md index 09f9321b..4de14194 100644 --- a/apps/docs/learn/state-management-patterns.md +++ b/apps/docs/learn/state-management-patterns.md @@ -17,7 +17,7 @@ No special configuration is needed on your `Bloc` or `Cubit` class for shared st import { Bloc } from '@blac/core'; // Define UserState, UserAction, initialUserState appropriately -export class UserBloc extends Bloc { +export class UserBloc extends Bloc { constructor() { super(initialUserState); } @@ -50,9 +50,9 @@ function SettingsPage() { ### Best For -- Global application state (e.g., user authentication, theme, global settings). -- State that needs to be consistently synchronized between distinct components. -- Features where multiple components interact with or display the same slice of data. +- Global application state (e.g., user authentication, theme, global settings). +- State that needs to be consistently synchronized between distinct components. +- Features where multiple components interact with or display the same slice of data. ## 2. Isolated State @@ -66,7 +66,10 @@ There are times when you need each component (or a specific part of your UI) to // src/blocs/WidgetSettingsCubit.ts import { Cubit } from '@blac/core'; - interface SettingsState { color: string; fontSize: number; } + interface SettingsState { + color: string; + fontSize: number; + } export class WidgetSettingsCubit extends Cubit { static isolated = true; // Each component gets its own instance @@ -80,19 +83,25 @@ There are times when you need each component (or a specific part of your UI) to } ``` -2. **Dynamic ID with `useBloc`**: Provide a unique `id` string in the `options` argument of `useBloc`. +2. **Dynamic ID with `useBloc`**: Provide a unique `instanceId` string in the `options` argument of `useBloc`. ```tsx // src/components/ConfigurableWidget.tsx import { useBloc } from '@blac/react'; import { WidgetSettingsCubit } from '../blocs/WidgetSettingsCubit'; - function ConfigurableWidget({ widgetId, initialColor }: { widgetId: string; initialColor?: string }) { + function ConfigurableWidget({ + widgetId, + initialColor, + }: { + widgetId: string; + initialColor?: string; + }) { // WidgetSettingsCubit does NOT need `static isolated = true` for this to work. - // The unique `id` ensures a distinct instance for this widgetId. + // The unique `instanceId` ensures a distinct instance for this widgetId. const [settings, settingsCubit] = useBloc(WidgetSettingsCubit, { - id: `widget-settings-${widgetId}`, - props: initialColor // Assuming constructor takes props for initialColor + instanceId: `widget-settings-${widgetId}`, + staticProps: initialColor, // Assuming constructor takes props for initialColor }); // ... render widget based on settings ... } @@ -107,7 +116,7 @@ If `WidgetSettingsCubit` has `static isolated = true;`: function App() { return ( <> - + ); @@ -116,12 +125,12 @@ function App() { ### Best For -- Components that require their own, non-shared state (e.g., a reusable form Bloc, settings for multiple instances of a widget on one page). -- Avoiding state conflicts when multiple instances of the same component are rendered. +- Components that require their own, non-shared state (e.g., a reusable form Bloc, settings for multiple instances of a widget on one page). +- Avoiding state conflicts when multiple instances of the same component are rendered. ## 3. In-Memory Persistence (`keepAlive`) -Normally, a shared `Bloc` or `Cubit` is disposed of when it no longer has any active listeners (i.e., components using it via `useBloc` have unmounted). If you need a shared instance to persist in memory *even when no components are currently using it*, you can set `static keepAlive = true;`. +Normally, a shared `Bloc` or `Cubit` is disposed of when it no longer has any active listeners (i.e., components using it via `useBloc` have unmounted). If you need a shared instance to persist in memory _even when no components are currently using it_, you can set `static keepAlive = true;`. This is useful for caching data, managing background tasks, or maintaining state across navigations where components might unmount and remount later, expecting the state to be preserved. @@ -131,7 +140,10 @@ This is useful for caching data, managing background tasks, or maintaining state // src/blocs/DataCacheBloc.ts import { Cubit } from '@blac/core'; -interface CacheState { data: Record | null; isLoading: boolean; } +interface CacheState { + data: Record | null; + isLoading: boolean; +} export class DataCacheBloc extends Cubit { static keepAlive = true; // Instance persists in memory @@ -141,8 +153,12 @@ export class DataCacheBloc extends Cubit { this.loadInitialData(); // Example: load data on init } - loadInitialData = async () => { /* ... */ } - fetchData = async (key: string) => { /* ... update state ... */ }; + loadInitialData = async () => { + /* ... */ + }; + fetchData = async (key: string) => { + /* ... update state ... */ + }; getCachedData = (key: string) => this.state.data?.[key]; } ``` @@ -153,9 +169,9 @@ When a component using `DataCacheBloc` unmounts, the `DataCacheBloc` instance (a ### Best For -- Caching data that is expensive to fetch, across component lifecycles or navigation. -- Managing application-wide services or settings that should always be available. -- Background tasks that need to maintain state independently of the UI. +- Caching data that is expensive to fetch, across component lifecycles or navigation. +- Managing application-wide services or settings that should always be available. +- Background tasks that need to maintain state independently of the UI. **Note**: `keepAlive` prevents disposal from lack of listeners. It does not inherently save state to disk or browser storage. @@ -169,7 +185,9 @@ To persist state across browser sessions (e.g., to `localStorage` or `sessionSto // src/blocs/ThemeCubit.ts import { Cubit, Persist } from '@blac/core'; -interface ThemeState { mode: 'light' | 'dark'; } +interface ThemeState { + mode: 'light' | 'dark'; +} export class ThemeCubit extends Cubit { // Connect the Persist addon @@ -191,10 +209,10 @@ export class ThemeCubit extends Cubit { ### Key Points for Storage Persistence: -- Use an addon like `Persist` (or create your own). -- Configure the addon (e.g., with a storage key, storage type). -- The addon typically handles loading state from storage on initialization and saving state to storage on changes. -- You might combine this with `static keepAlive = true;` if you want the instance managing the persisted state to also stay in memory regardless of listeners. +- Use an addon like `Persist` (or create your own). +- Configure the addon (e.g., with a storage key, storage type). +- The addon typically handles loading state from storage on initialization and saving state to storage on changes. +- You might combine this with `static keepAlive = true;` if you want the instance managing the persisted state to also stay in memory regardless of listeners. Refer to documentation on specific addons (like `Persist`) for detailed setup and options. @@ -206,12 +224,13 @@ You can combine these patterns. For example, an isolated Bloc that also stays al // src/blocs/UserTaskBloc.ts import { Bloc } from '@blac/core'; -export class UserTaskBloc extends Bloc { +export class UserTaskBloc extends Bloc { static isolated = true; static keepAlive = true; // ... } ``` + This would create a unique `UserTaskBloc` for each component instance that requests it, and each of those unique instances would persist in memory even if its originating component unmounts. ## Choosing the Right Pattern @@ -219,14 +238,14 @@ This would create a unique `UserTaskBloc` for each component instance that reque Consider these questions: 1. **Shared vs. Unique Instance?** - * Multiple components need the *exact same* state instance: Use **Shared State** (default). - * Each component (or context) needs its *own independent* state: Use **Isolated State** (via `static isolated` or dynamic `id` in `useBloc`). + - Multiple components need the _exact same_ state instance: Use **Shared State** (default). + - Each component (or context) needs its _own independent_ state: Use **Isolated State** (via `static isolated` or dynamic `id` in `useBloc`). 2. **Lifecycle when No Components Listen?** - * State/Instance can be discarded if nothing is listening: Default behavior (no `keepAlive`). - * State/Instance *must remain in memory* even if nothing is listening: Use **In-Memory Persistence (`keepAlive`)**. + - State/Instance can be discarded if nothing is listening: Default behavior (no `keepAlive`). + - State/Instance _must remain in memory_ even if nothing is listening: Use **In-Memory Persistence (`keepAlive`)**. 3. **Persistence Across Browser Sessions?** - * State should be saved to `localStorage`/`sessionStorage` and reloaded: Use **Storage Persistence (Addons)** like `Persist`. + - State should be saved to `localStorage`/`sessionStorage` and reloaded: Use **Storage Persistence (Addons)** like `Persist`. -By understanding these distinctions, you can architect your state management effectively with Blac. \ No newline at end of file +By understanding these distinctions, you can architect your state management effectively with Blac. diff --git a/apps/docs/package.json b/apps/docs/package.json index dce8760b..d0f849ae 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,7 +3,11 @@ "version": "1.0.0", "description": "Documentation for Blac state management library", "scripts": { - "dev:docs": "vitepress dev", + "format": "prettier --write \".\"", + "lint": "eslint . --ext .ts,.tsx,.js", + "lint:fix": "eslint . --ext .ts,.tsx,.js --fix", + "typecheck": "tsc --noEmit", + "dev": "vitepress dev", "build:docs": "vitepress build", "preview:docs": "vitepress preview" }, @@ -19,10 +23,11 @@ "vitepress": "^1.6.3" }, "dependencies": { + "prettier": "catalog:", "@braintree/sanitize-url": "^7.1.1", "dayjs": "^1.11.13", - "debug": "^4.4.0", - "mermaid": "^11.5.0", + "debug": "^4.4.1", + "mermaid": "^11.9.0", "vitepress-plugin-mermaid": "^2.0.17" } -} \ No newline at end of file +} diff --git a/apps/docs/plugins/api-reference.md b/apps/docs/plugins/api-reference.md new file mode 100644 index 00000000..89c1f6c0 --- /dev/null +++ b/apps/docs/plugins/api-reference.md @@ -0,0 +1,557 @@ +# Plugin API Reference + +Complete API reference for BlaC's plugin system, including interfaces, types, and methods. + +## Core Interfaces + +### Plugin + +Base interface for all plugins: + +```typescript +interface Plugin { + readonly name: string; + readonly version: string; + readonly capabilities?: PluginCapabilities; +} +``` + +### PluginCapabilities + +Declares what a plugin can do: + +```typescript +interface PluginCapabilities { + readonly readState: boolean; // Can read bloc state + readonly transformState: boolean; // Can modify state before it's applied + readonly interceptEvents: boolean; // Can intercept and modify events + readonly persistData: boolean; // Can persist data externally + readonly accessMetadata: boolean; // Can access internal bloc metadata +} +``` + +## System Plugin API + +### BlacPlugin Interface + +System-wide plugins that observe all blocs: + +```typescript +interface BlacPlugin extends Plugin { + // Lifecycle hooks + beforeBootstrap?(): void; + afterBootstrap?(): void; + beforeShutdown?(): void; + afterShutdown?(): void; + + // Bloc lifecycle observations + onBlocCreated?(bloc: BlocBase): void; + onBlocDisposed?(bloc: BlocBase): void; + + // State observations + onStateChanged?( + bloc: BlocBase, + previousState: any, + currentState: any, + ): void; + + // Event observations (Bloc only, not Cubit) + onEventAdded?(bloc: Bloc, event: any): void; + + // Error handling + onError?(error: Error, bloc: BlocBase, context: ErrorContext): void; + + // React adapter hooks + onAdapterCreated?(adapter: any, metadata: AdapterMetadata): void; + onAdapterMount?(adapter: any, metadata: AdapterMetadata): void; + onAdapterUnmount?(adapter: any, metadata: AdapterMetadata): void; + onAdapterRender?(adapter: any, metadata: AdapterMetadata): void; + onAdapterDisposed?(adapter: any, metadata: AdapterMetadata): void; +} +``` + +### ErrorContext + +Context provided when errors occur: + +```typescript +interface ErrorContext { + readonly phase: + | 'initialization' + | 'state-change' + | 'event-processing' + | 'disposal'; + readonly operation: string; + readonly metadata?: Record; +} +``` + +### AdapterMetadata + +Metadata about React component adapters: + +```typescript +interface AdapterMetadata { + componentName?: string; + blocInstance: BlocBase; + renderCount: number; + trackedPaths?: string[]; + isUsingDependencies?: boolean; + lastState?: any; + lastDependencyValues?: any[]; + currentDependencyValues?: any[]; +} +``` + +## Bloc Plugin API + +### BlocPlugin Interface + +Instance-specific plugins attached to individual blocs: + +```typescript +interface BlocPlugin extends Plugin { + // Transform hooks - can modify data + transformState?(previousState: TState, nextState: TState): TState; + transformEvent?(event: TEvent): TEvent | null; + + // Lifecycle hooks + onAttach?(bloc: BlocBase): void; + onDetach?(): void; + + // Observation hooks + onStateChange?(previousState: TState, currentState: TState): void; + onEvent?(event: TEvent): void; + onError?(error: Error, context: ErrorContext): void; +} +``` + +## Plugin Registry APIs + +### System Plugin Registry + +Global registry for system plugins: + +```typescript +class SystemPluginRegistry { + // Add a plugin + add(plugin: BlacPlugin): void; + + // Remove a plugin by name + remove(pluginName: string): boolean; + + // Get a specific plugin + get(pluginName: string): BlacPlugin | undefined; + + // Get all plugins + getAll(): ReadonlyArray; + + // Clear all plugins + clear(): void; + + // Bootstrap all plugins + bootstrap(): void; + + // Shutdown all plugins + shutdown(): void; + + // Notify plugins of bloc lifecycle events + notifyBlocCreated(bloc: BlocBase): void; + notifyBlocDisposed(bloc: BlocBase): void; +} +``` + +Access via `Blac.instance.plugins`: + +```typescript +import { Blac } from '@blac/core'; + +Blac.instance.plugins.add(myPlugin); +Blac.instance.plugins.remove('plugin-name'); +const plugin = Blac.instance.plugins.get('plugin-name'); +const all = Blac.instance.plugins.getAll(); +``` + +### Bloc Plugin Registry + +Instance-level registry for bloc plugins: + +```typescript +class BlocPluginRegistry { + // Add a plugin + add(plugin: BlocPlugin): void; + + // Remove a plugin by name + remove(pluginName: string): boolean; + + // Get a specific plugin + get(pluginName: string): BlocPlugin | undefined; + + // Get all plugins + getAll(): ReadonlyArray>; + + // Clear all plugins + clear(): void; +} +``` + +Access via bloc instance methods: + +```typescript +bloc.addPlugin(myPlugin); +bloc.removePlugin('plugin-name'); +const plugin = bloc.getPlugin('plugin-name'); +const all = bloc.getPlugins(); +``` + +## Hook Execution Order + +### System Plugin Hooks + +1. **Bootstrap Phase** + + ``` + beforeBootstrap() → afterBootstrap() + ``` + +2. **Bloc Creation** + + ``` + onBlocCreated(bloc) + ``` + +3. **State Change** + + ``` + onStateChanged(bloc, prev, curr) + ``` + +4. **Event Processing** (Bloc only) + + ``` + onEventAdded(bloc, event) + ``` + +5. **Error Handling** + + ``` + onError(error, bloc, context) + ``` + +6. **React Integration** + + ``` + onAdapterCreated() → onAdapterMount() → + onAdapterRender() → onAdapterUnmount() → + onAdapterDisposed() + ``` + +7. **Bloc Disposal** + + ``` + onBlocDisposed(bloc) + ``` + +8. **Shutdown Phase** + ``` + beforeShutdown() → afterShutdown() + ``` + +### Bloc Plugin Hooks + +1. **Attachment** + + ``` + onAttach(bloc) + ``` + +2. **Event Transform** (Bloc only) + + ``` + transformEvent(event) → [event processed] → onEvent(event) + ``` + +3. **State Transform** + + ``` + transformState(prev, next) → [state applied] → onStateChange(prev, curr) + ``` + +4. **Error Handling** + + ``` + onError(error, context) + ``` + +5. **Detachment** + ``` + onDetach() + ``` + +## Transform Hook Behavior + +### State Transformation + +```typescript +transformState?(previousState: TState, nextState: TState): TState; +``` + +- Called **before** state is applied +- Return modified state to change what gets applied +- Return `previousState` to reject the update +- Throwing an error prevents the state change + +Example: + +```typescript +transformState(prev, next) { + // Validate + if (!isValid(next)) { + return prev; // Reject invalid state + } + + // Transform + return { + ...next, + lastModified: Date.now() + }; +} +``` + +### Event Transformation + +```typescript +transformEvent?(event: TEvent): TEvent | null; +``` + +- Called **before** event is processed +- Return modified event to change what gets processed +- Return `null` to cancel the event +- Only available for `Bloc`, not `Cubit` + +Example: + +```typescript +transformEvent(event) { + // Filter + if (shouldIgnore(event)) { + return null; // Cancel event + } + + // Transform + return { + ...event, + timestamp: Date.now() + }; +} +``` + +## Performance Metrics + +### PluginExecutionContext + +Track plugin performance: + +```typescript +interface PluginExecutionContext { + readonly pluginName: string; + readonly hookName: string; + readonly startTime: number; + readonly blocName?: string; + readonly blocId?: string; +} +``` + +### PluginMetrics + +Performance statistics: + +```typescript +interface PluginMetrics { + readonly executionTime: number; + readonly executionCount: number; + readonly errorCount: number; + readonly lastError?: Error; + readonly lastExecutionTime?: number; +} +``` + +## Type Utilities + +### Generic Constraints + +```typescript +// Plugin that works with any state +class UniversalPlugin implements BlocPlugin { + // TState defaults to any +} + +// Plugin for specific state type +class TypedPlugin implements BlocPlugin { + // Constrained to UserState or subtypes +} + +// Plugin for specific event types +class EventPlugin implements BlocPlugin { + // Handles BaseEvent or subtypes +} +``` + +### Helper Types + +```typescript +// Extract state type from a bloc +type StateOf = T extends BlocBase ? S : never; + +// Extract event type from a bloc +type EventOf = T extends Bloc ? E : never; + +// Plugin for specific bloc type +class MyBlocPlugin implements BlocPlugin, EventOf> { + // Type-safe for MyBloc +} +``` + +## Error Handling + +### Plugin Errors + +Plugins should handle errors gracefully: + +```typescript +class SafePlugin implements BlacPlugin { + onStateChanged(bloc, prev, curr) { + try { + this.riskyOperation(curr); + } catch (error) { + // Log but don't throw + console.error(`Plugin error: ${error}`); + + // Optionally notify via error hook + this.onError?.(error as Error, bloc, { + phase: 'state-change', + operation: 'riskyOperation', + }); + } + } +} +``` + +### Error Recovery + +System continues even if plugins fail: + +1. Plugin errors are caught by BlaC +2. Error logged to console +3. Other plugins continue executing +4. Bloc operation completes normally + +## Security Model + +### Capability Enforcement + +Future versions will enforce declared capabilities: + +```typescript +class SecurePlugin implements BlacPlugin { + capabilities = { + readState: true, + transformState: false, // Can't modify + interceptEvents: false, + persistData: true, + accessMetadata: false, + }; + + onStateChanged(bloc, prev, curr) { + // ✅ Allowed: reading state + console.log(curr); + + // ❌ Future: would throw if attempting to modify + // bloc.emit(newState); // SecurityError + } +} +``` + +### Best Practices + +1. **Declare minimal capabilities** +2. **Sanitize sensitive data** +3. **Validate plugin sources** +4. **Use type constraints** +5. **Audit third-party plugins** + +## Migration Guide + +### From Custom Observers + +Before (custom pattern): + +```typescript +class MyBloc extends Bloc { + private observers: Observer[] = []; + + addObserver(observer: Observer) { + this.observers.push(observer); + } + + protected emit(state: State) { + super.emit(state); + this.observers.forEach((o) => o.onStateChange(state)); + } +} +``` + +After (plugin system): + +```typescript +class ObserverPlugin implements BlocPlugin { + onStateChange(prev, curr) { + // Same logic here + } +} + +bloc.addPlugin(new ObserverPlugin()); +``` + +### From Middleware + +Before (middleware pattern): + +```typescript +const withLogging = (bloc: BlocBase) => { + const originalEmit = bloc.emit; + bloc.emit = (state) => { + console.log('State:', state); + originalEmit.call(bloc, state); + }; +}; +``` + +After (plugin system): + +```typescript +class LoggingPlugin implements BlocPlugin { + onStateChange(prev, curr) { + console.log('State:', curr); + } +} +``` + +## Future APIs + +Planned additions to the plugin API: + +- **Async hooks** for network operations +- **Priority levels** for execution order +- **Plugin dependencies** and ordering +- **Conditional activation** based on environment +- **Performance profiling** built-in +- **Plugin composition** utilities + +## See Also + +- [Plugin Overview](./overview.md) +- [Creating Plugins](./creating-plugins.md) +- [System Plugins](./system-plugins.md) +- [Bloc Plugins](./bloc-plugins.md) +- [Persistence Plugin](./persistence.md) diff --git a/apps/docs/plugins/bloc-plugins.md b/apps/docs/plugins/bloc-plugins.md new file mode 100644 index 00000000..e5f9016a --- /dev/null +++ b/apps/docs/plugins/bloc-plugins.md @@ -0,0 +1,570 @@ +# Bloc Plugins + +Bloc plugins (implementing `BlocPlugin`) are attached to specific bloc instances, allowing you to add custom behavior to individual blocs without modifying their core logic. + +## Creating a Bloc Plugin + +To create a bloc plugin, implement the `BlocPlugin` interface: + +```typescript +import { BlocPlugin, PluginCapabilities, ErrorContext } from '@blac/core'; + +class MyBlocPlugin + implements BlocPlugin +{ + // Required properties + readonly name = 'my-bloc-plugin'; + readonly version = '1.0.0'; + + // Optional capabilities + readonly capabilities: PluginCapabilities = { + readState: true, + transformState: true, + interceptEvents: true, + persistData: false, + accessMetadata: false, + }; + + // Transform hooks - can modify data + transformState(previousState: TState, nextState: TState): TState { + // Modify state before it's applied + return nextState; + } + + transformEvent(event: TEvent): TEvent | null { + // Modify or filter events (null to cancel) + return event; + } + + // Lifecycle hooks + onAttach(bloc: BlocBase) { + console.log(`Plugin attached to ${bloc._name}`); + } + + onDetach() { + console.log('Plugin detached'); + } + + // Observation hooks + onStateChange(previousState: TState, currentState: TState) { + console.log('State changed:', { from: previousState, to: currentState }); + } + + onEvent(event: TEvent) { + console.log('Event processed:', event); + } + + onError(error: Error, context: ErrorContext) { + console.error(`Error during ${context.phase}:`, error); + } +} +``` + +## Attaching Plugins to Blocs + +Attach plugins during bloc creation or afterwards: + +```typescript +// Method 1: During creation +class MyBloc extends Bloc { + constructor() { + super(initialState); + + // Add plugin + this.addPlugin(new MyBlocPlugin()); + } +} + +// Method 2: After creation +const bloc = new MyBloc(); +bloc.addPlugin(new MyBlocPlugin()); + +// Remove a plugin +bloc.removePlugin('my-bloc-plugin'); + +// Get a plugin +const plugin = bloc.getPlugin('my-bloc-plugin'); + +// Get all plugins +const plugins = bloc.getPlugins(); +``` + +## Common Use Cases + +### 1. State Validation Plugin + +```typescript +interface ValidationRule { + validate: (state: T) => boolean; + message: string; +} + +class ValidationPlugin implements BlocPlugin { + name = 'validation'; + version = '1.0.0'; + + constructor(private rules: ValidationRule[]) {} + + transformState(previousState: TState, nextState: TState): TState { + // Validate state before applying + for (const rule of this.rules) { + if (!rule.validate(nextState)) { + console.error(`Validation failed: ${rule.message}`); + // Return previous state to prevent invalid update + return previousState; + } + } + return nextState; + } +} + +// Usage +class UserBloc extends Cubit { + constructor() { + super(initialState); + + this.addPlugin( + new ValidationPlugin([ + { + validate: (state) => state.email.includes('@'), + message: 'Email must be valid', + }, + { + validate: (state) => state.age >= 0, + message: 'Age cannot be negative', + }, + ]), + ); + } +} +``` + +### 2. Undo/Redo Plugin + +```typescript +class UndoRedoPlugin implements BlocPlugin { + name = 'undo-redo'; + version = '1.0.0'; + + private history: TState[] = []; + private currentIndex = -1; + private maxHistory = 50; + private bloc?: BlocBase; + + onAttach(bloc: BlocBase) { + this.bloc = bloc; + this.history = [bloc.state]; + this.currentIndex = 0; + } + + onStateChange(previousState: TState, currentState: TState) { + // Add to history if not from undo/redo + if (!this.isUndoRedo) { + this.currentIndex++; + this.history = this.history.slice(0, this.currentIndex); + this.history.push(currentState); + + // Limit history size + if (this.history.length > this.maxHistory) { + this.history.shift(); + this.currentIndex--; + } + } + this.isUndoRedo = false; + } + + private isUndoRedo = false; + + undo() { + if (this.currentIndex > 0 && this.bloc) { + this.currentIndex--; + this.isUndoRedo = true; + (this.bloc as any).emit(this.history[this.currentIndex]); + } + } + + redo() { + if (this.currentIndex < this.history.length - 1 && this.bloc) { + this.currentIndex++; + this.isUndoRedo = true; + (this.bloc as any).emit(this.history[this.currentIndex]); + } + } + + get canUndo() { + return this.currentIndex > 0; + } + + get canRedo() { + return this.currentIndex < this.history.length - 1; + } +} + +// Usage +const bloc = new EditorBloc(); +const undoRedo = new UndoRedoPlugin(); +bloc.addPlugin(undoRedo); + +// Later in UI + +``` + +### 3. Computed Properties Plugin + +```typescript +class ComputedPlugin implements BlocPlugin { + name = 'computed'; + version = '1.0.0'; + + private computedValues = new Map(); + private computers = new Map any>(); + + constructor(computers: Record any>) { + Object.entries(computers).forEach(([key, computer]) => { + this.computers.set(key, computer); + }); + } + + onStateChange(previousState: TState, currentState: TState) { + // Recompute values + this.computers.forEach((computer, key) => { + const newValue = computer(currentState); + this.computedValues.set(key, newValue); + }); + } + + get(key: string): T | undefined { + return this.computedValues.get(key); + } + + getAll(): Record { + return Object.fromEntries(this.computedValues); + } +} + +// Usage +interface CartState { + items: Array<{ price: number; quantity: number }>; + taxRate: number; +} + +const cartBloc = new CartBloc(); +const computed = new ComputedPlugin({ + subtotal: (state) => + state.items.reduce((sum, item) => sum + item.price * item.quantity, 0), + tax: (state) => { + const subtotal = state.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + return subtotal * state.taxRate; + }, + total: (state) => { + const subtotal = state.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + return subtotal * (1 + state.taxRate); + }, +}); + +cartBloc.addPlugin(computed); + +// Access computed values +const total = computed.get('total'); +``` + +### 4. Event Filtering Plugin + +```typescript +class EventFilterPlugin implements BlocPlugin { + name = 'event-filter'; + version = '1.0.0'; + + constructor( + private filter: (event: TEvent) => boolean, + private onFiltered?: (event: TEvent) => void, + ) {} + + transformEvent(event: TEvent): TEvent | null { + if (this.filter(event)) { + return event; // Allow event + } + + // Event filtered out + this.onFiltered?.(event); + return null; // Cancel event + } +} + +// Usage: Rate limiting +class RateLimitPlugin extends EventFilterPlugin< + TState, + TEvent +> { + private lastEventTime = new Map(); + + constructor( + private getEventType: (event: TEvent) => string, + private minInterval: number = 1000, + ) { + super((event) => { + const type = this.getEventType(event); + const now = Date.now(); + const last = this.lastEventTime.get(type) || 0; + + if (now - last >= this.minInterval) { + this.lastEventTime.set(type, now); + return true; + } + return false; + }); + } +} + +// Apply rate limiting to search +class SearchBloc extends Bloc { + constructor() { + super(initialState); + + this.addPlugin( + new RateLimitPlugin( + (event) => event.constructor.name, + 500, // Min 500ms between same event types + ), + ); + } +} +``` + +## Advanced Patterns + +### 1. Plugin Composition + +Combine multiple plugins for complex behavior: + +```typescript +class CompositePlugin implements BlocPlugin { + name = 'composite'; + version = '1.0.0'; + + constructor(private plugins: BlocPlugin[]) {} + + transformState(prev: TState, next: TState): TState { + return this.plugins.reduce( + (state, plugin) => plugin.transformState?.(prev, state) ?? state, + next, + ); + } + + onAttach(bloc: BlocBase) { + this.plugins.forEach((p) => p.onAttach?.(bloc)); + } + + onStateChange(prev: TState, curr: TState) { + this.plugins.forEach((p) => p.onStateChange?.(prev, curr)); + } +} +``` + +### 2. Async Plugin Operations + +Handle async operations carefully: + +```typescript +class AsyncPersistencePlugin implements BlocPlugin { + name = 'async-persistence'; + version = '1.0.0'; + + private saveQueue: TState[] = []; + private saving = false; + + onStateChange(prev: TState, curr: TState) { + this.saveQueue.push(curr); + this.processSaveQueue(); + } + + private async processSaveQueue() { + if (this.saving || this.saveQueue.length === 0) return; + + this.saving = true; + const state = this.saveQueue.pop()!; + this.saveQueue = []; // Skip intermediate states + + try { + await this.saveState(state); + } catch (error) { + console.error('Failed to save state:', error); + } finally { + this.saving = false; + + // Process any new states that arrived + if (this.saveQueue.length > 0) { + this.processSaveQueue(); + } + } + } + + private async saveState(state: TState) { + // Async save operation + } +} +``` + +### 3. Plugin Communication + +Enable plugins to communicate: + +```typescript +interface PluginMessage { + from: string; + to: string; + type: string; + data: any; +} + +class MessagingPlugin implements BlocPlugin { + name = 'messaging'; + version = '1.0.0'; + + private bloc?: BlocBase; + private handlers = new Map void>(); + + onAttach(bloc: BlocBase) { + this.bloc = bloc; + } + + send(to: string, type: string, data: any) { + const targetPlugin = this.bloc?.getPlugin(to); + if (targetPlugin && 'onMessage' in targetPlugin) { + (targetPlugin as any).onMessage({ + from: this.name, + to, + type, + data, + }); + } + } + + onMessage(message: PluginMessage) { + const handler = this.handlers.get(message.type); + handler?.(message); + } + + on(type: string, handler: (message: PluginMessage) => void) { + this.handlers.set(type, handler); + } +} +``` + +## Best Practices + +### 1. State Immutability + +Always return new state objects: + +```typescript +class ImmutablePlugin implements BlocPlugin { + name = 'immutable'; + version = '1.0.0'; + + transformState(prev: TState, next: TState): TState { + // Ensure new reference + if (prev === next) { + console.warn('State mutation detected!'); + return { ...next }; // Force new object + } + return next; + } +} +``` + +### 2. Error Handling + +Handle errors gracefully: + +```typescript +class SafePlugin implements BlocPlugin { + name = 'safe'; + version = '1.0.0'; + + transformState(prev: TState, next: TState): TState { + try { + return this.validateState(next); + } catch (error) { + console.error('Plugin error:', error); + return prev; // Fallback to previous state + } + } +} +``` + +### 3. Performance Optimization + +Minimize overhead in frequently called methods: + +```typescript +class OptimizedPlugin implements BlocPlugin { + name = 'optimized'; + version = '1.0.0'; + + private cache = new WeakMap(); + + transformState(prev: TState, next: TState): TState { + // Use caching for expensive operations + if (this.cache.has(next)) { + return this.cache.get(next); + } + + const processed = this.expensiveProcess(next); + this.cache.set(next, processed); + return processed; + } +} +``` + +## Testing Bloc Plugins + +Test plugins in isolation and with blocs: + +```typescript +describe('ValidationPlugin', () => { + let bloc: UserBloc; + let plugin: ValidationPlugin; + + beforeEach(() => { + plugin = new ValidationPlugin([ + { + validate: (state) => state.age >= 0, + message: 'Age must be positive', + }, + ]); + + bloc = new UserBloc(); + bloc.addPlugin(plugin); + }); + + it('prevents invalid state updates', () => { + const initialAge = bloc.state.age; + + // Try to set negative age + bloc.updateAge(-5); + + // State should not change + expect(bloc.state.age).toBe(initialAge); + }); + + it('allows valid state updates', () => { + bloc.updateAge(25); + expect(bloc.state.age).toBe(25); + }); +}); +``` + +## Next Steps + +- Explore the [Persistence Plugin](./persistence.md) for a real-world example +- Learn about [System Plugins](./system-plugins.md) for global functionality +- Check the [Plugin API Reference](./api-reference.md) for complete details diff --git a/apps/docs/plugins/creating-plugins.md b/apps/docs/plugins/creating-plugins.md new file mode 100644 index 00000000..b05b9b02 --- /dev/null +++ b/apps/docs/plugins/creating-plugins.md @@ -0,0 +1,716 @@ +# Creating Plugins + +Learn how to create custom plugins to extend BlaC's functionality for your specific needs. + +## Plugin Basics + +Every plugin in BlaC implements either the `BlacPlugin` interface (for system-wide functionality) or the `BlocPlugin` interface (for bloc-specific functionality). + +### Anatomy of a Plugin + +```typescript +import { Plugin, PluginCapabilities } from '@blac/core'; + +class MyPlugin implements Plugin { + // Required: Unique identifier + readonly name = 'my-plugin'; + + // Required: Semantic version + readonly version = '1.0.0'; + + // Optional: Declare capabilities + readonly capabilities: PluginCapabilities = { + readState: true, + transformState: false, + interceptEvents: false, + persistData: false, + accessMetadata: false, + }; +} +``` + +## System Plugin Tutorial + +Let's create a performance monitoring plugin that tracks state update frequency. + +### Step 1: Define the Plugin Structure + +```typescript +import { BlacPlugin, BlocBase, Bloc } from '@blac/core'; + +interface PerformanceMetrics { + stateChanges: number; + eventsProcessed: number; + lastUpdate: number; + updateFrequency: number[]; // Updates per second over time +} + +export class PerformanceMonitorPlugin implements BlacPlugin { + readonly name = 'performance-monitor'; + readonly version = '1.0.0'; + readonly capabilities = { + readState: true, + transformState: false, + interceptEvents: false, + persistData: false, + accessMetadata: true, + }; + + private metrics = new Map(); + private updateTimers = new Map(); +} +``` + +### Step 2: Implement Lifecycle Hooks + +```typescript +export class PerformanceMonitorPlugin implements BlacPlugin { + // ... previous code ... + + onBlocCreated(bloc: BlocBase) { + const key = this.getBlocKey(bloc); + this.metrics.set(key, { + stateChanges: 0, + eventsProcessed: 0, + lastUpdate: Date.now(), + updateFrequency: [], + }); + this.updateTimers.set(key, []); + } + + onBlocDisposed(bloc: BlocBase) { + const key = this.getBlocKey(bloc); + this.metrics.delete(key); + this.updateTimers.delete(key); + } + + private getBlocKey(bloc: BlocBase): string { + return `${bloc._name}:${bloc._id}`; + } +} +``` + +### Step 3: Track State Changes + +```typescript +export class PerformanceMonitorPlugin implements BlacPlugin { + // ... previous code ... + + onStateChanged(bloc: BlocBase, previousState: any, currentState: any) { + const key = this.getBlocKey(bloc); + const metrics = this.metrics.get(key); + if (!metrics) return; + + const now = Date.now(); + const timeSinceLastUpdate = now - metrics.lastUpdate; + + // Update metrics + metrics.stateChanges++; + metrics.lastUpdate = now; + + // Track update frequency + const timers = this.updateTimers.get(key)!; + timers.push(now); + + // Keep only last minute of data + const oneMinuteAgo = now - 60000; + const recentTimers = timers.filter((t) => t > oneMinuteAgo); + this.updateTimers.set(key, recentTimers); + + // Calculate updates per second + if (recentTimers.length > 1) { + const duration = (now - recentTimers[0]) / 1000; // seconds + const frequency = recentTimers.length / duration; + metrics.updateFrequency.push(frequency); + + // Keep only last 60 data points + if (metrics.updateFrequency.length > 60) { + metrics.updateFrequency.shift(); + } + } + } + + onEventAdded(bloc: Bloc, event: any) { + const key = this.getBlocKey(bloc); + const metrics = this.metrics.get(key); + if (metrics) { + metrics.eventsProcessed++; + } + } +} +``` + +### Step 4: Add Reporting Methods + +```typescript +export class PerformanceMonitorPlugin implements BlacPlugin { + // ... previous code ... + + getMetrics(bloc: BlocBase): PerformanceMetrics | undefined { + return this.metrics.get(this.getBlocKey(bloc)); + } + + getAllMetrics(): Map { + return new Map(this.metrics); + } + + getReport(): string { + const reports: string[] = []; + + this.metrics.forEach((metrics, key) => { + const avgFrequency = + metrics.updateFrequency.length > 0 + ? metrics.updateFrequency.reduce((a, b) => a + b, 0) / + metrics.updateFrequency.length + : 0; + + reports.push(` + Bloc: ${key} + State Changes: ${metrics.stateChanges} + Events Processed: ${metrics.eventsProcessed} + Avg Update Frequency: ${avgFrequency.toFixed(2)}/sec + `); + }); + + return reports.join('\n---\n'); + } + + // High-frequency detection + getHighFrequencyBlocs(threshold = 10): string[] { + const results: string[] = []; + + this.metrics.forEach((metrics, key) => { + const recent = metrics.updateFrequency.slice(-10); + const avgRecent = + recent.length > 0 + ? recent.reduce((a, b) => a + b, 0) / recent.length + : 0; + + if (avgRecent > threshold) { + results.push(`${key} (${avgRecent.toFixed(1)}/sec)`); + } + }); + + return results; + } +} +``` + +### Step 5: Use the Plugin + +```typescript +import { Blac } from '@blac/core'; +import { PerformanceMonitorPlugin } from './PerformanceMonitorPlugin'; + +// Register globally +const perfMonitor = new PerformanceMonitorPlugin(); +Blac.instance.plugins.add(perfMonitor); + +// Later, get performance report +console.log(perfMonitor.getReport()); + +// Check for performance issues +const highFreq = perfMonitor.getHighFrequencyBlocs(); +if (highFreq.length > 0) { + console.warn('High frequency updates detected:', highFreq); +} +``` + +## Bloc Plugin Tutorial + +Let's create a computed properties plugin that automatically calculates derived values. + +### Step 1: Define the Plugin + +```typescript +import { BlocPlugin, BlocBase } from '@blac/core'; + +type ComputedFunction = (state: TState) => any; + +interface ComputedConfig { + [key: string]: ComputedFunction; +} + +export class ComputedPropertiesPlugin implements BlocPlugin { + readonly name = 'computed-properties'; + readonly version = '1.0.0'; + + private computedValues = new Map(); + private bloc?: BlocBase; + + constructor(private computers: ComputedConfig) {} +} +``` + +### Step 2: Implement Lifecycle Methods + +```typescript +export class ComputedPropertiesPlugin implements BlocPlugin { + // ... previous code ... + + onAttach(bloc: BlocBase) { + this.bloc = bloc; + // Calculate initial values + this.recalculate(bloc.state); + } + + onDetach() { + this.bloc = undefined; + this.computedValues.clear(); + } + + onStateChange(previousState: TState, currentState: TState) { + this.recalculate(currentState); + } + + private recalculate(state: TState) { + Object.entries(this.computers).forEach(([key, computer]) => { + try { + const value = computer(state); + this.computedValues.set(key, value); + } catch (error) { + console.error(`Error computing ${key}:`, error); + this.computedValues.set(key, undefined); + } + }); + } +} +``` + +### Step 3: Add Access Methods + +```typescript +export class ComputedPropertiesPlugin implements BlocPlugin { + // ... previous code ... + + get(key: string): T | undefined { + return this.computedValues.get(key); + } + + getAll(): Record { + return Object.fromEntries(this.computedValues); + } + + // Create a proxy for dot notation access + get values(): Record { + return new Proxy( + {}, + { + get: (_, prop: string) => this.get(prop), + }, + ); + } +} +``` + +### Step 4: Use the Plugin + +```typescript +// Define your state and cubit +interface CartState { + items: Array<{ + id: string; + name: string; + price: number; + quantity: number; + }>; + taxRate: number; + discountPercent: number; +} + +class CartCubit extends Cubit { + constructor() { + super({ + items: [], + taxRate: 0.08, + discountPercent: 0 + }); + + // Add computed properties + const computed = new ComputedPropertiesPlugin({ + subtotal: (state) => + state.items.reduce((sum, item) => + sum + (item.price * item.quantity), 0 + ), + + discount: (state) => { + const subtotal = state.items.reduce((sum, item) => + sum + (item.price * item.quantity), 0 + ); + return subtotal * (state.discountPercent / 100); + }, + + taxableAmount: (state) => { + const subtotal = state.items.reduce((sum, item) => + sum + (item.price * item.quantity), 0 + ); + const discount = subtotal * (state.discountPercent / 100); + return subtotal - discount; + }, + + tax: (state) => { + const subtotal = state.items.reduce((sum, item) => + sum + (item.price * item.quantity), 0 + ); + const discount = subtotal * (state.discountPercent / 100); + const taxable = subtotal - discount; + return taxable * state.taxRate; + }, + + total: (state) => { + const subtotal = state.items.reduce((sum, item) => + sum + (item.price * item.quantity), 0 + ); + const discount = subtotal * (state.discountPercent / 100); + const taxable = subtotal - discount; + const tax = taxable * state.taxRate; + return taxable + tax; + }, + + itemCount: (state) => + state.items.reduce((sum, item) => sum + item.quantity, 0), + + isEmpty: (state) => state.items.length === 0 + }); + + this.addPlugin(computed); + + // Store reference for easy access + this.computed = computed; + } + + computed!: ComputedPropertiesPlugin; + + addItem = (item: CartState['items'][0]) => { + this.patch({ + items: [...this.state.items, item] + }); + }; + + setDiscount = (percent: number) => { + this.patch({ discountPercent: percent }); + }; +} + +// Use in component +function ShoppingCart() { + const [state, cart] = useBloc(CartCubit); + + return ( +
    +

    Shopping Cart ({cart.computed.get('itemCount')} items)

    + +
    Subtotal: ${cart.computed.values.subtotal.toFixed(2)}
    +
    Discount: -${cart.computed.values.discount.toFixed(2)}
    +
    Tax: ${cart.computed.values.tax.toFixed(2)}
    +
    Total: ${cart.computed.values.total.toFixed(2)}
    + + {cart.computed.get('isEmpty') && ( +

    Your cart is empty

    + )} +
    + ); +} +``` + +## Advanced Plugin Patterns + +### Conditional Activation + +```typescript +class ConditionalPlugin implements BlacPlugin { + name = 'conditional'; + version = '1.0.0'; + + private isActive = false; + + constructor(private condition: () => boolean) {} + + beforeBootstrap() { + this.isActive = this.condition(); + } + + onStateChanged(bloc, prev, curr) { + if (!this.isActive) return; + // Plugin logic here + } +} + +// Use based on environment +Blac.instance.plugins.add( + new ConditionalPlugin(() => process.env.NODE_ENV === 'development'), +); +``` + +### Plugin Communication + +```typescript +class CommunicatingPlugin implements BlacPlugin { + name = 'communicator'; + version = '1.0.0'; + + private handlers = new Map(); + + // Register message handler + on(event: string, handler: Function) { + this.handlers.set(event, handler); + } + + // Send message to other plugins + emit(event: string, data: any) { + // Find other plugins that can receive + const plugins = Blac.instance.plugins.getAll(); + plugins.forEach((plugin) => { + if ('onMessage' in plugin && plugin !== this) { + (plugin as any).onMessage(event, data, this.name); + } + }); + } + + // Receive messages + onMessage(event: string, data: any, from: string) { + const handler = this.handlers.get(event); + handler?.(data, from); + } +} +``` + +### Async Operations in Plugins + +```typescript +class AsyncPlugin implements BlocPlugin { + name = 'async-plugin'; + version = '1.0.0'; + + private pendingOperations = new Set>(); + + onStateChange(prev: TState, curr: TState) { + // Don't block state updates + const operation = this.performAsyncWork(curr).finally(() => + this.pendingOperations.delete(operation), + ); + + this.pendingOperations.add(operation); + } + + private async performAsyncWork(state: TState) { + try { + // Async operations here + await fetch('/api/analytics', { + method: 'POST', + body: JSON.stringify(state), + }); + } catch (error) { + console.error('Plugin async error:', error); + } + } + + onDetach() { + // Cancel pending operations + this.pendingOperations.clear(); + } +} +``` + +## Testing Your Plugins + +### Unit Testing + +```typescript +import { BlocTest, Cubit } from '@blac/core'; +import { PerformanceMonitorPlugin } from './PerformanceMonitorPlugin'; + +describe('PerformanceMonitorPlugin', () => { + let plugin: PerformanceMonitorPlugin; + + beforeEach(() => { + BlocTest.setUp(); + plugin = new PerformanceMonitorPlugin(); + Blac.instance.plugins.add(plugin); + }); + + afterEach(() => { + BlocTest.tearDown(); + }); + + it('tracks state changes', () => { + class TestCubit extends Cubit<{ count: number }> { + constructor() { + super({ count: 0 }); + } + increment = () => this.emit({ count: this.state.count + 1 }); + } + + const cubit = new TestCubit(); + + // Trigger state changes + cubit.increment(); + cubit.increment(); + cubit.increment(); + + const metrics = plugin.getMetrics(cubit); + expect(metrics?.stateChanges).toBe(3); + }); + + it('detects high frequency updates', async () => { + class RapidCubit extends Cubit<{ value: number }> { + constructor() { + super({ value: 0 }); + } + update = () => this.emit({ value: Math.random() }); + } + + const cubit = new RapidCubit(); + + // Rapid updates + const interval = setInterval(cubit.update, 50); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + clearInterval(interval); + + const highFreq = plugin.getHighFrequencyBlocs(); + expect(highFreq.length).toBeGreaterThan(0); + }); +}); +``` + +### Integration Testing + +```typescript +describe('ComputedPropertiesPlugin Integration', () => { + it('works with React components', () => { + const { result } = renderHook(() => useBloc(CartCubit)); + const [, cart] = result.current; + + act(() => { + cart.addItem({ + id: '1', + name: 'Widget', + price: 10, + quantity: 2, + }); + }); + + expect(cart.computed.get('subtotal')).toBe(20); + expect(cart.computed.get('itemCount')).toBe(2); + }); +}); +``` + +## Best Practices + +### 1. Performance Considerations + +- Keep plugin operations lightweight +- Avoid blocking operations in sync hooks +- Use debouncing for expensive calculations +- Clean up resources in lifecycle hooks + +### 2. Error Handling + +```typescript +class ResilientPlugin implements BlacPlugin { + name = 'resilient'; + version = '1.0.0'; + + onStateChanged(bloc, prev, curr) { + try { + this.riskyOperation(curr); + } catch (error) { + // Log but don't crash + console.error(`${this.name} error:`, error); + + // Optional: Report to error tracking + this.reportError(error, bloc); + } + } + + private reportError(error: unknown, bloc: BlocBase) { + // Send to error tracking service + } +} +``` + +### 3. Type Safety + +```typescript +// Use generics for type safety +export function createTypedPlugin() { + return class TypedPlugin implements BlocPlugin { + name = 'typed-plugin'; + version = '1.0.0'; + + onStateChange(prev: TState, curr: TState) { + // Full type safety here + } + }; +} + +// Usage +const MyPlugin = createTypedPlugin(); +bloc.addPlugin(new MyPlugin()); +``` + +### 4. Documentation + +Always document your plugin's: + +- Purpose and use cases +- Configuration options +- Performance characteristics +- Any side effects +- Example usage + +## Publishing Your Plugin + +### Package Structure + +``` +my-blac-plugin/ +├── src/ +│ ├── index.ts +│ ├── MyPlugin.ts +│ └── __tests__/ +├── package.json +├── tsconfig.json +├── README.md +└── LICENSE +``` + +### Package.json + +```json +{ + "name": "@myorg/blac-plugin-example", + "version": "1.0.0", + "description": "Example plugin for BlaC state management", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist"], + "peerDependencies": { + "@blac/core": "^2.0.0" + }, + "keywords": ["blac", "plugin", "state-management"], + "license": "MIT" +} +``` + +### Export Pattern + +```typescript +// index.ts +export * from './MyPlugin'; +export * from './types'; + +// Optional: Provide factory function +export function createMyPlugin(options?: MyPluginOptions) { + return new MyPlugin(options); +} +``` + +## Next Steps + +- Explore the [Plugin API Reference](./api-reference.md) +- See real examples in [System Plugins](./system-plugins.md) +- Learn from the [Persistence Plugin](./persistence.md) implementation +- Share your plugins with the community! diff --git a/apps/docs/plugins/overview.md b/apps/docs/plugins/overview.md new file mode 100644 index 00000000..373b4149 --- /dev/null +++ b/apps/docs/plugins/overview.md @@ -0,0 +1,90 @@ +# Plugin System + +BlaC's plugin system provides a powerful way to extend the functionality of your state management without modifying core code. Plugins can observe state changes, transform state, intercept events, and add custom behavior to your blocs and cubits. + +## Overview + +The plugin system in BlaC is designed with several key principles: + +- **Non-intrusive**: Plugins observe and enhance without breaking existing functionality +- **Type-safe**: Full TypeScript support with proper type inference +- **Performance-conscious**: Minimal overhead with synchronous hooks +- **Secure**: Capability-based security model controls what plugins can access +- **Extensible**: Both system-wide and bloc-specific plugins + +## Types of Plugins + +BlaC supports two types of plugins: + +### 1. System Plugins (BlacPlugin) + +System plugins observe all blocs in your application. They're perfect for: + +- Global logging and debugging +- Analytics and monitoring +- Development tools +- Cross-cutting concerns + +### 2. Bloc Plugins (BlocPlugin) + +Bloc plugins are attached to specific bloc instances. They're ideal for: + +- State persistence +- State transformation +- Event interception +- Instance-specific behavior + +## Quick Example + +Here's a simple logging plugin that tracks all state changes: + +```typescript +import { BlacPlugin, BlacLifecycleEvent } from '@blac/core'; + +class LoggingPlugin implements BlacPlugin { + name = 'logger'; + version = '1.0.0'; + + onStateChanged(bloc, previousState, currentState) { + console.log(`[${bloc._name}] State changed:`, { + from: previousState, + to: currentState, + timestamp: Date.now(), + }); + } +} + +// Register the plugin globally +Blac.instance.plugins.add(new LoggingPlugin()); +``` + +## Plugin Capabilities + +Plugins declare their capabilities for security and clarity: + +```typescript +interface PluginCapabilities { + readState: boolean; // Can read bloc state + transformState: boolean; // Can modify state + interceptEvents: boolean; // Can intercept/modify events + persistData: boolean; // Can persist data externally + accessMetadata: boolean; // Can access internal metadata +} +``` + +## Installation + +Most plugins are distributed as npm packages: + +```bash +npm install @blac/plugin-persistence +npm install @blac/plugin-devtools +``` + +## What's Next? + +- [Creating Plugins](./creating-plugins.md) - Build your own plugins +- [System Plugins](./system-plugins.md) - Global plugin documentation +- [Bloc Plugins](./bloc-plugins.md) - Instance-specific plugins +- [Persistence Plugin](./persistence.md) - Built-in persistence plugin +- [Plugin API Reference](./api-reference.md) - Complete API documentation diff --git a/apps/docs/plugins/persistence.md b/apps/docs/plugins/persistence.md new file mode 100644 index 00000000..dd458915 --- /dev/null +++ b/apps/docs/plugins/persistence.md @@ -0,0 +1,594 @@ +# Persistence Plugin + +The Persistence Plugin provides automatic state persistence for your BlaC blocs and cubits. It saves state changes to storage and restores state when your application restarts. + +## Installation + +```bash +npm install @blac/plugin-persistence +``` + +## Quick Start + +```typescript +import { Cubit } from '@blac/core'; +import { PersistencePlugin } from '@blac/plugin-persistence'; + +class SettingsCubit extends Cubit { + constructor() { + super(defaultSettings); + + // Add persistence + this.addPlugin( + new PersistencePlugin({ + key: 'app-settings', + }), + ); + } +} +``` + +That's it! Your settings will now persist across app restarts. + +## Configuration Options + +```typescript +interface PersistenceOptions { + // Required: Storage key + key: string; + + // Storage adapter (defaults to localStorage) + storage?: StorageAdapter; + + // Custom serialization + serialize?: (state: TState) => string; + deserialize?: (data: string) => TState; + + // Debounce saves (ms) + debounceMs?: number; + + // Version for migrations + version?: string; + + // Selective persistence + select?: (state: TState) => Partial; + merge?: (persisted: Partial, current: TState) => TState; + + // Encryption + encrypt?: { + encrypt: (data: string) => string | Promise; + decrypt: (data: string) => string | Promise; + }; + + // Migrations from old keys + migrations?: Array<{ + from: string; + transform?: (oldState: any) => TState; + }>; + + // Error handling + onError?: (error: Error, operation: 'save' | 'load' | 'migrate') => void; +} +``` + +## Storage Adapters + +### Built-in Adapters + +```typescript +import { + getDefaultStorage, + createLocalStorage, + createSessionStorage, + createAsyncStorage, + createMemoryStorage, +} from '@blac/plugin-persistence'; + +// Browser localStorage (default) +const localStorage = createLocalStorage(); + +// Browser sessionStorage +const sessionStorage = createSessionStorage(); + +// React Native AsyncStorage +const asyncStorage = createAsyncStorage(); + +// In-memory storage (testing) +const memoryStorage = createMemoryStorage(); +``` + +### Custom Storage Adapter + +Implement the `StorageAdapter` interface: + +```typescript +interface StorageAdapter { + getItem(key: string): string | null | Promise; + setItem(key: string, value: string): void | Promise; + removeItem(key: string): void | Promise; +} + +// Example: IndexedDB adapter +class IndexedDBAdapter implements StorageAdapter { + async getItem(key: string): Promise { + const db = await this.openDB(); + const tx = db.transaction('store', 'readonly'); + const result = await tx.objectStore('store').get(key); + return result?.value ?? null; + } + + async setItem(key: string, value: string): Promise { + const db = await this.openDB(); + const tx = db.transaction('store', 'readwrite'); + await tx.objectStore('store').put({ key, value }); + } + + async removeItem(key: string): Promise { + const db = await this.openDB(); + const tx = db.transaction('store', 'readwrite'); + await tx.objectStore('store').delete(key); + } + + private async openDB() { + // IndexedDB setup logic + } +} +``` + +## Usage Examples + +### Basic Persistence + +```typescript +class TodoCubit extends Cubit { + constructor() { + super({ todos: [], filter: 'all' }); + + this.addPlugin( + new PersistencePlugin({ + key: 'todos', + }), + ); + } + + addTodo = (text: string) => { + this.patch({ + todos: [...this.state.todos, { id: Date.now(), text, done: false }], + }); + }; +} +``` + +### Selective Persistence + +Only persist specific parts of state: + +```typescript +class UserCubit extends Cubit { + constructor() { + super(initialState); + + this.addPlugin( + new PersistencePlugin({ + key: 'user-preferences', + + // Only persist preferences, not temporary UI state + select: (state) => ({ + theme: state.theme, + language: state.language, + notifications: state.notifications, + }), + + // Merge persisted data with current state + merge: (persisted, current) => ({ + ...current, + ...persisted, + }), + }), + ); + } +} +``` + +### Custom Serialization + +Handle complex data types: + +```typescript +class MapCubit extends Cubit<{ locations: Map }> { + constructor() { + super({ locations: new Map() }); + + this.addPlugin( + new PersistencePlugin({ + key: 'map-data', + + // Convert Map to JSON-serializable format + serialize: (state) => + JSON.stringify({ + locations: Array.from(state.locations.entries()), + }), + + // Restore Map from JSON + deserialize: (data) => { + const parsed = JSON.parse(data); + return { + locations: new Map(parsed.locations), + }; + }, + }), + ); + } +} +``` + +### Encrypted Storage + +Protect sensitive data: + +```typescript +import { encrypt, decrypt } from 'your-crypto-lib'; + +class AuthCubit extends Cubit { + constructor() { + super({ isAuthenticated: false, token: null }); + + this.addPlugin( + new PersistencePlugin({ + key: 'auth', + + encrypt: { + encrypt: async (data) => { + const key = await getEncryptionKey(); + return encrypt(data, key); + }, + decrypt: async (data) => { + const key = await getEncryptionKey(); + return decrypt(data, key); + }, + }, + }), + ); + } +} +``` + +### Migrations + +Handle data structure changes: + +```typescript +class SettingsCubit extends Cubit { + constructor() { + super(defaultSettingsV2); + + this.addPlugin( + new PersistencePlugin({ + key: 'settings-v2', + version: '2.0.0', + + migrations: [ + { + from: 'settings-v1', + transform: (oldSettings: SettingsV1): SettingsV2 => ({ + ...oldSettings, + newFeature: 'default-value', + // Map old structure to new + preferences: { + theme: oldSettings.isDarkMode ? 'dark' : 'light', + ...oldSettings.preferences, + }, + }), + }, + ], + }), + ); + } +} +``` + +### Debounced Saves + +Optimize performance for frequent updates: + +```typescript +class EditorCubit extends Cubit { + constructor() { + super({ content: '', cursor: 0 }); + + this.addPlugin( + new PersistencePlugin({ + key: 'editor-draft', + debounceMs: 1000, // Save at most once per second + }), + ); + } + + updateContent = (content: string) => { + // State updates immediately + this.patch({ content }); + // But saves are debounced + }; +} +``` + +### Error Handling + +Handle storage failures gracefully: + +```typescript +class DataCubit extends Cubit { + constructor() { + super(initialState); + + this.addPlugin( + new PersistencePlugin({ + key: 'app-data', + + onError: (error, operation) => { + console.error(`Storage ${operation} failed:`, error); + + if (operation === 'save') { + // Notify user that changes might not persist + this.showStorageWarning(); + } else if (operation === 'load') { + // Use default state if load fails + console.log('Using default state'); + } + }, + }), + ); + } +} +``` + +## Advanced Patterns + +### Multi-Storage Strategy + +Use different storage for different data: + +```typescript +class AppCubit extends Cubit { + constructor() { + super(initialState); + + // Critical data in localStorage + this.addPlugin( + new PersistencePlugin({ + key: 'app-critical', + storage: createLocalStorage(), + select: (state) => ({ + userId: state.userId, + settings: state.settings, + }), + }), + ); + + // Session data in sessionStorage + this.addPlugin( + new PersistencePlugin({ + key: 'app-session', + storage: createSessionStorage(), + select: (state) => ({ + currentView: state.currentView, + tempData: state.tempData, + }), + }), + ); + } +} +``` + +### Conditional Persistence + +Enable/disable persistence dynamically: + +```typescript +class FeatureCubit extends Cubit { + private persistencePlugin?: PersistencePlugin; + + constructor(private userPreferences: UserPreferences) { + super(initialState); + + if (userPreferences.enablePersistence) { + this.enablePersistence(); + } + } + + enablePersistence() { + if (!this.persistencePlugin) { + this.persistencePlugin = new PersistencePlugin({ + key: 'feature-state', + }); + this.addPlugin(this.persistencePlugin); + } + } + + disablePersistence() { + if (this.persistencePlugin) { + this.removePlugin(this.persistencePlugin.name); + this.persistencePlugin.clear(); // Clear stored data + this.persistencePlugin = undefined; + } + } +} +``` + +### Sync Across Tabs + +Sync state across browser tabs: + +```typescript +class SharedCubit extends Cubit { + constructor() { + super(initialState); + + const plugin = new PersistencePlugin({ + key: 'shared-state', + }); + + this.addPlugin(plugin); + + // Listen for storage events from other tabs + window.addEventListener('storage', (e) => { + if (e.key === 'shared-state' && e.newValue) { + const newState = JSON.parse(e.newValue); + // Update state without triggering another save + (this as any).emit(newState); + } + }); + } +} +``` + +## Best Practices + +### 1. Choose Appropriate Keys + +Use descriptive, unique keys: + +```typescript +// Good +new PersistencePlugin({ key: 'app-user-preferences-v1' }); + +// Bad +new PersistencePlugin({ key: 'data' }); +``` + +### 2. Version Your Storage + +Plan for future changes: + +```typescript +new PersistencePlugin({ + key: 'user-data-v2', + version: '2.0.0', + migrations: [{ from: 'user-data-v1' }, { from: 'legacy-user-data' }], +}); +``` + +### 3. Handle Sensitive Data + +Never store sensitive data in plain text: + +```typescript +// Bad +this.patch({ password: userPassword }); + +// Good - store tokens with encryption +this.patch({ authToken: encryptedToken }); +``` + +### 4. Consider Storage Limits + +Be mindful of storage constraints: + +```typescript +class CacheCubit extends Cubit { + constructor() { + super({ items: [] }); + + this.addPlugin( + new PersistencePlugin({ + key: 'cache', + select: (state) => ({ + // Limit stored items + items: state.items.slice(-100), + }), + }), + ); + } +} +``` + +### 5. Test Persistence + +Test with different storage states: + +```typescript +describe('PersistenceCubit', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('restores saved state', async () => { + // Save state + const cubit1 = new MyCubit(); + cubit1.updateValue('test'); + + // Simulate app restart + const cubit2 = new MyCubit(); + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(cubit2.state.value).toBe('test'); + }); + + it('handles corrupted storage', () => { + localStorage.setItem('my-key', 'invalid-json'); + + const cubit = new MyCubit(); + // Should use initial state + expect(cubit.state).toEqual(initialState); + }); +}); +``` + +## API Reference + +### PersistencePlugin + +```typescript +class PersistencePlugin implements BlocPlugin { + constructor(options: PersistenceOptions); + + // Clear stored state + clear(): Promise; +} +``` + +### Storage Adapters + +```typescript +interface StorageAdapter { + getItem(key: string): string | null | Promise; + setItem(key: string, value: string): void | Promise; + removeItem(key: string): void | Promise; +} + +// Factory functions +function createLocalStorage(): StorageAdapter; +function createSessionStorage(): StorageAdapter; +function createAsyncStorage(): StorageAdapter; +function createMemoryStorage(): StorageAdapter; +function getDefaultStorage(): StorageAdapter; +``` + +## Troubleshooting + +### State Not Persisting + +1. Check browser storage is not disabled +2. Verify key uniqueness +3. Check for errors in console +4. Ensure state is serializable + +### Performance Issues + +1. Increase debounce time +2. Use selective persistence +3. Limit stored data size +4. Consider async storage adapter + +### Migration Failures + +1. Test migrations thoroughly +2. Keep old keys during transition +3. Provide fallback values +4. Log migration errors + +## Next Steps + +- Learn about [Creating Custom Plugins](./creating-plugins.md) +- Explore other [Plugin Examples](./examples.md) +- Check the [Plugin API Reference](./api-reference.md) diff --git a/apps/docs/plugins/system-plugins.md b/apps/docs/plugins/system-plugins.md new file mode 100644 index 00000000..d9c1f44b --- /dev/null +++ b/apps/docs/plugins/system-plugins.md @@ -0,0 +1,430 @@ +# System Plugins + +System plugins (implementing `BlacPlugin`) observe and interact with all bloc instances in your application. They're registered globally and receive lifecycle notifications for every bloc. + +## Creating a System Plugin + +To create a system plugin, implement the `BlacPlugin` interface: + +```typescript +import { + BlacPlugin, + BlocBase, + ErrorContext, + AdapterMetadata, +} from '@blac/core'; + +class MySystemPlugin implements BlacPlugin { + // Required properties + readonly name = 'my-plugin'; + readonly version = '1.0.0'; + + // Optional capabilities declaration + readonly capabilities = { + readState: true, + transformState: false, + interceptEvents: false, + persistData: true, + accessMetadata: false, + }; + + // Lifecycle hooks + beforeBootstrap() { + console.log('Plugin system starting up'); + } + + afterBootstrap() { + console.log('Plugin system ready'); + } + + beforeShutdown() { + console.log('Plugin system shutting down'); + } + + afterShutdown() { + console.log('Plugin system shut down'); + } + + // Bloc lifecycle hooks + onBlocCreated(bloc: BlocBase) { + console.log(`Bloc created: ${bloc._name}`); + } + + onBlocDisposed(bloc: BlocBase) { + console.log(`Bloc disposed: ${bloc._name}`); + } + + // State observation + onStateChanged(bloc: BlocBase, previousState: any, currentState: any) { + console.log(`State changed in ${bloc._name}`, { + from: previousState, + to: currentState, + }); + } + + // Event observation (for Bloc instances only) + onEventAdded(bloc: Bloc, event: any) { + console.log(`Event added to ${bloc._name}:`, event); + } + + // Error handling + onError(error: Error, bloc: BlocBase, context: ErrorContext) { + console.error(`Error in ${bloc._name} during ${context.phase}:`, error); + } + + // React adapter hooks + onAdapterCreated(adapter: any, metadata: AdapterMetadata) { + console.log('React component connected', metadata.componentName); + } + + onAdapterRender(adapter: any, metadata: AdapterMetadata) { + console.log(`Component rendered (${metadata.renderCount} times)`); + } +} +``` + +## Registering System Plugins + +Register system plugins using the global plugin registry: + +```typescript +import { Blac } from '@blac/core'; + +// Add a plugin +const plugin = new MySystemPlugin(); +Blac.instance.plugins.add(plugin); + +// Remove a plugin +Blac.instance.plugins.remove('my-plugin'); + +// Get a specific plugin +const myPlugin = Blac.instance.plugins.get('my-plugin'); + +// Get all plugins +const allPlugins = Blac.instance.plugins.getAll(); + +// Clear all plugins +Blac.instance.plugins.clear(); +``` + +## Common Use Cases + +### 1. Development Logger + +```typescript +class DevLoggerPlugin implements BlacPlugin { + name = 'dev-logger'; + version = '1.0.0'; + + onStateChanged(bloc, previousState, currentState) { + if (process.env.NODE_ENV !== 'development') return; + + console.groupCollapsed(`[${bloc._name}] State Update`); + console.log('Previous:', previousState); + console.log('Current:', currentState); + console.log('Time:', new Date().toISOString()); + console.groupEnd(); + } + + onError(error, bloc, context) { + console.error(`[${bloc._name}] Error in ${context.phase}:`, error); + } +} +``` + +### 2. Analytics Tracker + +```typescript +class AnalyticsPlugin implements BlacPlugin { + name = 'analytics'; + version = '1.0.0'; + + private analytics: AnalyticsService; + + constructor(analytics: AnalyticsService) { + this.analytics = analytics; + } + + onBlocCreated(bloc) { + this.analytics.track('bloc_created', { + bloc_type: bloc._name, + timestamp: Date.now(), + }); + } + + onStateChanged(bloc, previousState, currentState) { + this.analytics.track('state_changed', { + bloc_type: bloc._name, + state_snapshot: this.sanitizeState(currentState), + }); + } + + private sanitizeState(state: any) { + // Remove sensitive data before tracking + const { password, token, ...safe } = state; + return safe; + } +} +``` + +### 3. Performance Monitor + +```typescript +class PerformancePlugin implements BlacPlugin { + name = 'performance'; + version = '1.0.0'; + + private metrics = new Map(); + + onStateChanged(bloc, previousState, currentState) { + const start = performance.now(); + + // Track state change performance + process.nextTick(() => { + const duration = performance.now() - start; + this.recordMetric(bloc._name, 'stateChange', duration); + }); + } + + onAdapterRender(adapter, metadata) { + this.recordMetric( + metadata.blocInstance._name, + 'render', + metadata.renderCount, + ); + } + + private recordMetric(blocName: string, metric: string, value: number) { + if (!this.metrics.has(blocName)) { + this.metrics.set(blocName, {}); + } + // Record performance data + } + + getReport() { + return Array.from(this.metrics.entries()); + } +} +``` + +### 4. State Snapshot Plugin + +```typescript +class SnapshotPlugin implements BlacPlugin { + name = 'snapshot'; + version = '1.0.0'; + + private snapshots = new Map(); + private maxSnapshots = 10; + + onStateChanged(bloc, previousState, currentState) { + const key = `${bloc._name}:${bloc._id}`; + + if (!this.snapshots.has(key)) { + this.snapshots.set(key, []); + } + + const history = this.snapshots.get(key)!; + history.push({ + state: currentState, + timestamp: Date.now(), + }); + + // Keep only recent snapshots + if (history.length > this.maxSnapshots) { + history.shift(); + } + } + + getHistory(bloc: BlocBase) { + const key = `${bloc._name}:${bloc._id}`; + return this.snapshots.get(key) || []; + } + + clearHistory() { + this.snapshots.clear(); + } +} +``` + +## Best Practices + +### 1. Minimal Performance Impact + +System plugins are called frequently, so keep operations lightweight: + +```typescript +class EfficientPlugin implements BlacPlugin { + name = 'efficient'; + version = '1.0.0'; + + private pendingUpdates = new Map(); + + onStateChanged(bloc, prev, curr) { + // Batch updates instead of immediate processing + this.pendingUpdates.set(bloc._name, { prev, curr }); + + if (!this.flushScheduled) { + this.flushScheduled = true; + setImmediate(() => this.flush()); + } + } + + private flush() { + // Process all updates at once + this.pendingUpdates.forEach((update, blocName) => { + this.processUpdate(blocName, update); + }); + this.pendingUpdates.clear(); + this.flushScheduled = false; + } +} +``` + +### 2. Error Boundaries + +Always handle errors in plugins to prevent cascading failures: + +```typescript +class SafePlugin implements BlacPlugin { + name = 'safe'; + version = '1.0.0'; + + onStateChanged(bloc, prev, curr) { + try { + this.riskyOperation(bloc, curr); + } catch (error) { + console.error(`Plugin error in ${this.name}:`, error); + // Don't throw - let the app continue + } + } +} +``` + +### 3. Conditional Activation + +Enable plugins based on environment or configuration: + +```typescript +// Only in development +if (process.env.NODE_ENV === 'development') { + Blac.instance.plugins.add(new DevLoggerPlugin()); +} + +// Feature flag +if (config.features.analytics) { + Blac.instance.plugins.add(new AnalyticsPlugin(analyticsService)); +} +``` + +### 4. Plugin Ordering + +Plugins execute in registration order. Register critical plugins first: + +```typescript +// Register in priority order +Blac.instance.plugins.add(new ErrorHandlerPlugin()); // First - catch errors +Blac.instance.plugins.add(new PerformancePlugin()); // Second - measure performance +Blac.instance.plugins.add(new LoggerPlugin()); // Third - log events +``` + +## Plugin Lifecycle + +Understanding when hooks are called helps you choose the right one: + +1. **Bootstrap Phase** + - `beforeBootstrap()` - Before any initialization + - `afterBootstrap()` - System ready + +2. **Bloc Creation** + - `onBlocCreated()` - Immediately after bloc instantiation + +3. **State Changes** + - `onStateChanged()` - After state is updated and subscribers notified + +4. **Event Processing** (Bloc only) + - `onEventAdded()` - When event is added to processing queue + +5. **React Integration** + - `onAdapterCreated()` - When component first uses bloc + - `onAdapterMount()` - Component mounted + - `onAdapterRender()` - Each render + - `onAdapterUnmount()` - Component unmounted + - `onAdapterDisposed()` - Adapter cleaned up + +6. **Disposal** + - `onBlocDisposed()` - Bloc is being cleaned up + +7. **Shutdown** + - `beforeShutdown()` - System shutting down + - `afterShutdown()` - Cleanup complete + +## Testing System Plugins + +Test plugins in isolation: + +```typescript +import { BlocTest } from '@blac/core'; + +describe('MyPlugin', () => { + let plugin: MyPlugin; + + beforeEach(() => { + BlocTest.setUp(); + plugin = new MyPlugin(); + Blac.instance.plugins.add(plugin); + }); + + afterEach(() => { + BlocTest.tearDown(); + }); + + it('tracks state changes', () => { + const bloc = new CounterCubit(); + bloc.increment(); + + expect(plugin.getStateChangeCount()).toBe(1); + }); +}); +``` + +## Security Considerations + +System plugins have broad access. Consider: + +1. **Capability Declaration**: Always declare capabilities accurately +2. **Data Sanitization**: Remove sensitive data before logging/tracking +3. **Access Control**: Validate plugin sources in production +4. **Resource Limits**: Prevent memory leaks with data caps + +```typescript +class SecurePlugin implements BlacPlugin { + name = 'secure'; + version = '1.0.0'; + + capabilities = { + readState: true, + transformState: false, // Can't modify state + interceptEvents: false, // Can't block events + persistData: true, // Can save externally + accessMetadata: false, // No internal access + }; + + onStateChanged(bloc, prev, curr) { + // Sanitize sensitive data + const safe = this.removeSensitiveData(curr); + this.logStateChange(bloc._name, safe); + } + + private removeSensitiveData(state: any) { + const { password, token, ssn, ...safe } = state; + return safe; + } +} +``` + +## Next Steps + +- Learn about [Bloc Plugins](./bloc-plugins.md) for instance-specific functionality +- Explore the [Persistence Plugin](./persistence.md) implementation +- See the [Plugin API Reference](./api-reference.md) for complete details diff --git a/apps/docs/public/logo.svg b/apps/docs/public/logo.svg index 688c4413..17c221d3 100644 --- a/apps/docs/public/logo.svg +++ b/apps/docs/public/logo.svg @@ -1,11 +1,62 @@ - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + diff --git a/apps/docs/react/hooks.md b/apps/docs/react/hooks.md new file mode 100644 index 00000000..45eb2685 --- /dev/null +++ b/apps/docs/react/hooks.md @@ -0,0 +1,170 @@ +# React Hooks + +This section provides a concise reference for BlaC's React hooks. For detailed API documentation, see the [Hooks API Reference](/api/react/hooks). + +## Available Hooks + +### useBloc + +Connect components to state containers: + +```typescript +const [state, cubit] = useBloc(CounterCubit); +``` + +With options: + +```typescript +const [state, cubit] = useBloc(UserCubit, { + instanceId: 'user-123', // Custom instance ID + staticProps: { userId: '123' }, // Constructor props + dependencies: (bloc) => [userId], // Re-create on change +}); +``` + +### useValue + +Get state without the instance: + +```typescript +const count = useValue(CounterCubit); +const { todos, filter } = useValue(TodoCubit); +``` + +### createBloc + +Create a Cubit with setState API: + +```typescript +const FormBloc = createBloc({ + name: '', + email: '', + isValid: false, +}); + +class FormManager extends FormBloc { + updateField = (field: string, value: string) => { + this.setState({ [field]: value }); + }; +} +``` + +## Quick Examples + +### Counter + +```typescript +function Counter() { + const [count, cubit] = useBloc(CounterCubit); + + return ( + + ); +} +``` + +### Todo List + +```typescript +function TodoList() { + const [{ todos, isLoading }, cubit] = useBloc(TodoCubit); + + if (isLoading) return ; + + return ( +
      + {todos.map(todo => ( +
    • + cubit.toggle(todo.id)} + /> + {todo.text} +
    • + ))} +
    + ); +} +``` + +### Form + +```typescript +function LoginForm() { + const [state, form] = useBloc(LoginFormCubit); + + return ( +
    { e.preventDefault(); form.submit(); }}> + form.setEmail(e.target.value)} + /> + +
    + ); +} +``` + +## Performance Tips + +1. **Automatic Optimization**: Components only re-render when accessed properties change +2. **Multiple Instances**: Use `id` option for independent instances +3. **Memoization**: Use `useMemo` for expensive computations +4. **Isolation**: Set `static isolated = true` for component-specific state + +## Common Patterns + +### Custom Hooks + +```typescript +function useAuth() { + const [state, bloc] = useBloc(AuthBloc); + + return { + user: state.user, + isAuthenticated: state.isAuthenticated, + login: bloc.login, + logout: bloc.logout, + }; +} +``` + +### Conditional Rendering + +```typescript +function UserProfile() { + const [state] = useBloc(UserCubit); + + if (state.isLoading) return ; + if (state.error) return ; + if (!state.user) return ; + + return ; +} +``` + +### Effects + +```typescript +function DataLoader({ id }: { id: string }) { + const [state, cubit] = useBloc(DataCubit); + + useEffect(() => { + cubit.load(id); + }, [id, cubit]); + + return ; +} +``` + +## See Also + +- [Hooks API Reference](/api/react/hooks) - Complete API documentation +- [React Patterns](/react/patterns) - Advanced patterns and best practices +- [Getting Started](/getting-started/first-cubit) - Basic usage tutorial diff --git a/apps/docs/react/patterns.md b/apps/docs/react/patterns.md new file mode 100644 index 00000000..ccee5198 --- /dev/null +++ b/apps/docs/react/patterns.md @@ -0,0 +1,677 @@ +# React Patterns + +Learn best practices and common patterns for using BlaC effectively in your React applications. + +## Component Organization + +### Component Splitting for Performance + +Split your UI into focused components that each use `useBloc` directly. This eliminates prop drilling and provides automatic performance optimization: + +```typescript +// Main container component +function TodoApp() { + return ( +
    +

    Todo List

    + + + + +
    + ); +} + +// Each component accesses only the state it needs +function AddTodoForm() { + const [state, todoCubit] = useBloc(TodoCubit); + + return ( +
    + todoCubit.setInputText(e.target.value)} + placeholder="What needs to be done?" + /> + +
    + ); +} + +function FilterButtons() { + const [state, todoCubit] = useBloc(TodoCubit); + + return ( +
    + + + +
    + ); +} + +function TodoList() { + const [state, todoCubit] = useBloc(TodoCubit); + + return ( +
      + {todoCubit.filteredTodos.map(todo => ( +
    • + todoCubit.toggleTodo(todo.id)} + /> + {todo.text} + +
    • + ))} +
    + ); +} + +function ClearCompletedButton() { + const [state, todoCubit] = useBloc(TodoCubit); + const hasCompletedTodos = state.todos.some(todo => todo.completed); + + if (!hasCompletedTodos) { + return null; + } + + return ( + + ); +} +``` + +**Benefits of this pattern:** + +1. **Automatic Performance Optimization**: Each component only re-renders when the specific state or getters it uses change +2. **No Prop Drilling**: Components directly access what they need via `useBloc` +3. **Better Maintainability**: Components are self-contained and can be moved around easily +4. **Cleaner Code**: No need to pass callbacks or state through multiple component layers + +For example: + +- `AddTodoForm` only re-renders when `inputText` changes +- `FilterButtons` only re-renders when `filter` or `activeTodoCount` changes +- `TodoList` only re-renders when `filteredTodos` changes +- `ClearCompletedButton` only re-renders when the presence of completed todos changes + +### Feature-Based Structure + +Organize by feature rather than file type: + +``` +src/ +├── features/ +│ ├── auth/ +│ │ ├── AuthBloc.ts +│ │ ├── AuthGuard.tsx +│ │ ├── LoginForm.tsx +│ │ └── useAuth.ts +│ ├── todos/ +│ │ ├── TodoCubit.ts +│ │ ├── TodoList.tsx +│ │ ├── TodoItem.tsx +│ │ └── useTodos.ts +│ └── settings/ +│ ├── SettingsCubit.ts +│ ├── SettingsPanel.tsx +│ └── useSettings.ts +``` + +## Custom Hooks + +### Basic Custom Hook + +Encapsulate BlaC usage in custom hooks: + +```typescript +// hooks/useCounter.ts +export function useCounter(instanceId?: string) { + const [count, cubit] = useBloc(CounterCubit, { instanceId }); + + const increment = useCallback(() => cubit.increment(), [cubit]); + const decrement = useCallback(() => cubit.decrement(), [cubit]); + const reset = useCallback(() => cubit.reset(), [cubit]); + + return { + count, + increment, + decrement, + reset, + isEven: count % 2 === 0 + }; +} + +// Usage +function Counter() { + const { count, increment, isEven } = useCounter(); + + return ( +
    +

    Count: {count} {isEven && '(even)'}

    + +
    + ); +} +``` + +### Advanced Hook with Effects + +```typescript +export function useUserProfile(userId: string) { + const [state, cubit] = useBloc(UserCubit, { + instanceId: `user-${userId}`, + staticProps: { userId }, + }); + + // Load user on mount and userId change + useEffect(() => { + cubit.load(userId); + }, [userId, cubit]); + + // Refresh every 5 minutes + useEffect(() => { + const interval = setInterval( + () => { + cubit.refresh(); + }, + 5 * 60 * 1000, + ); + + return () => clearInterval(interval); + }, [cubit]); + + return { + user: state.user, + isLoading: state.isLoading, + error: state.error, + refresh: cubit.refresh, + }; +} +``` + +### Combining Multiple Blocs + +```typescript +export function useAppState() { + const auth = useAuth(); + const theme = useTheme(); + const notifications = useNotifications(); + + return { + isReady: auth.isAuthenticated && !auth.isLoading, + currentUser: auth.user, + isDarkMode: theme.mode === 'dark', + unreadCount: notifications.unread.length, + + // Combined actions + logout: () => { + auth.logout(); + notifications.clear(); + theme.reset(); + }, + }; +} +``` + +## State Sharing Patterns + +### Global Singleton + +Default behavior - one instance shared everywhere: + +```typescript +// Shared across entire app +class AppSettingsCubit extends Cubit { + // No special configuration needed +} + +// Both components share the same instance +function Header() { + const [settings] = useBloc(AppSettingsCubit); +} + +function Footer() { + const [settings] = useBloc(AppSettingsCubit); +} +``` + +### Isolated Instances + +Each component gets its own instance: + +```typescript +class FormCubit extends Cubit { + static isolated = true; // Each usage gets unique instance +} + +// Independent instances +function FormA() { + const [state] = useBloc(FormCubit); // Instance A +} + +function FormB() { + const [state] = useBloc(FormCubit); // Instance B +} +``` + +### Scoped Instances + +Share within a specific component tree: + +```typescript +function FeatureRoot() { + const featureId = useId(); + + return ( + + + + + ); +} + +function FeatureComponent() { + const featureId = useContext(FeatureContext); + const [state] = useBloc(FeatureCubit, { instanceId: featureId }); + // All components in this feature share the same instance +} +``` + +## Performance Patterns + +### Optimized Re-renders + +BlaC automatically tracks dependencies: + +```typescript +function TodoItem({ id }: { id: string }) { + const [state] = useBloc(TodoCubit); + + // Only re-renders when this specific todo changes + const todo = state.todos.find(t => t.id === id); + + if (!todo) return null; + + return
    {todo.text}
    ; +} +``` + +### Memoized Selectors + +For expensive computations: + +```typescript +function TodoStats() { + const [state] = useBloc(TodoCubit); + + const stats = useMemo(() => { + const completed = state.todos.filter(t => t.completed); + const active = state.todos.filter(t => !t.completed); + + return { + total: state.todos.length, + completed: completed.length, + active: active.length, + percentComplete: state.todos.length > 0 + ? Math.round((completed.length / state.todos.length) * 100) + : 0 + }; + }, [state.todos]); + + return ; +} +``` + +### Lazy Initialization + +Defer expensive initialization: + +```typescript +class ExpensiveDataCubit extends Cubit { + constructor() { + super({ data: null, isInitialized: false }); + } + + initialize = once(async () => { + const data = await this.loadExpensiveData(); + this.patch({ data, isInitialized: true }); + }); +} + +function DataComponent() { + const [state, cubit] = useBloc(ExpensiveDataCubit); + + useEffect(() => { + cubit.initialize(); + }, [cubit]); + + if (!state.isInitialized) return ; + return ; +} +``` + +## Form Patterns + +### Controlled Forms + +```typescript +class ContactFormCubit extends Cubit { + constructor() { + super({ + values: { name: '', email: '', message: '' }, + errors: {}, + touched: {}, + isSubmitting: false + }); + } + + updateField = (field: keyof FormValues, value: string) => { + this.patch({ + values: { ...this.state.values, [field]: value }, + touched: { ...this.state.touched, [field]: true } + }); + this.validateField(field, value); + }; + + validateField = (field: string, value: string) => { + const errors = { ...this.state.errors }; + + switch (field) { + case 'email': + errors.email = !value.includes('@') ? 'Invalid email' : undefined; + break; + case 'name': + errors.name = !value.trim() ? 'Required' : undefined; + break; + } + + this.patch({ errors }); + }; +} + +function ContactForm() { + const [state, form] = useBloc(ContactFormCubit); + + return ( +
    { e.preventDefault(); form.submit(); }}> + form.updateField('name', value)} + /> + {/* More fields */} + + ); +} +``` + +### Multi-Step Forms + +```typescript +class WizardCubit extends Cubit { + constructor() { + super({ + currentStep: 0, + steps: ['personal', 'contact', 'review'], + data: {}, + errors: {}, + }); + } + + nextStep = () => { + if (this.validateCurrentStep()) { + this.patch({ currentStep: this.state.currentStep + 1 }); + } + }; + + previousStep = () => { + this.patch({ currentStep: Math.max(0, this.state.currentStep - 1) }); + }; + + updateStepData = (data: Partial) => { + this.patch({ + data: { ...this.state.data, ...data }, + }); + }; +} +``` + +## Error Handling Patterns + +### Error Boundaries + +```typescript +class ErrorBoundary extends React.Component< + { children: React.ReactNode; fallback: React.ComponentType<{ error: Error }> }, + { hasError: boolean; error: Error | null } +> { + state = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + // Send to error tracking service + } + + render() { + if (this.state.hasError && this.state.error) { + return ; + } + + return this.props.children; + } +} + +// Usage +function App() { + return ( + + + + ); +} +``` + +### Global Error Handling + +```typescript +class GlobalErrorCubit extends Cubit { + constructor() { + super({ errors: [] }); + } + + addError = (error: AppError) => { + const id = Date.now(); + this.patch({ + errors: [...this.state.errors, { ...error, id }] + }); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + this.removeError(id); + }, 5000); + }; + + removeError = (id: number) => { + this.patch({ + errors: this.state.errors.filter(e => e.id !== id) + }); + }; +} + +// Global error display +function ErrorToasts() { + const [{ errors }] = useBloc(GlobalErrorCubit); + + return ( +
    + {errors.map(error => ( + + ))} +
    + ); +} +``` + +## Testing Patterns + +### Component Testing + +```typescript +import { render, screen, fireEvent } from '@testing-library/react'; +import { CounterCubit } from './CounterCubit'; + +// Mock the Cubit +jest.mock('./CounterCubit'); + +describe('Counter Component', () => { + let mockCubit: jest.Mocked; + + beforeEach(() => { + mockCubit = { + state: 0, + increment: jest.fn(), + decrement: jest.fn() + } as any; + + (CounterCubit as any).mockImplementation(() => mockCubit); + }); + + test('renders count and handles clicks', () => { + render(); + + expect(screen.getByText('Count: 0')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('+')); + expect(mockCubit.increment).toHaveBeenCalled(); + }); +}); +``` + +### Hook Testing + +```typescript +import { renderHook, act } from '@testing-library/react'; +import { useCounter } from './useCounter'; + +test('useCounter hook', () => { + const { result } = renderHook(() => useCounter()); + + expect(result.current.count).toBe(0); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(1); + expect(result.current.isEven).toBe(false); +}); +``` + +## Advanced Patterns + +### Middleware Pattern + +```typescript +function withLogging>( + BlocClass: new (...args: any[]) => T, +): new (...args: any[]) => T { + return class extends BlocClass { + constructor(...args: any[]) { + super(...args); + + // Use subscribe to listen for state changes + this.subscribe((state) => { + console.log(`[${this.constructor.name}] State changed:`, state); + }); + } + }; +} + +// Usage +const LoggedCounterCubit = withLogging(CounterCubit); +``` + +### Plugin System + +```typescript +interface CubitPlugin { + onInit?: (cubit: Cubit) => void; + onStateChange?: (state: S, previousState: S) => void; + onDispose?: () => void; +} + +class PluggableCubit extends Cubit { + private plugins: CubitPlugin[] = []; + private previousState: S; + + constructor(initialState: S) { + super(initialState); + this.previousState = initialState; + + // Subscribe to state changes + this.subscribe((state) => { + this.plugins.forEach((p) => p.onStateChange?.(state, this.previousState)); + this.previousState = state; + }); + } + + use(plugin: CubitPlugin) { + this.plugins.push(plugin); + plugin.onInit?.(this); + } + + dispose() { + this.plugins.forEach((p) => p.onDispose?.()); + super.dispose(); + } +} +``` + +## Best Practices + +1. **Keep Components Focused**: Each component should have a single responsibility +2. **Use Custom Hooks**: Encapsulate complex BlaC usage in custom hooks +3. **Prefer Composition**: Compose smaller Cubits/Blocs rather than creating large ones +4. **Test Business Logic**: Test Cubits/Blocs separately from components +5. **Handle Loading States**: Always provide feedback during async operations +6. **Error Boundaries**: Use error boundaries to catch and handle errors gracefully +7. **Type Everything**: Leverage TypeScript for better developer experience + +## See Also + +- [React Hooks](/react/hooks) - Hook API reference +- [Testing](/patterns/testing) - Testing strategies +- [Performance](/patterns/performance) - Performance optimization +- [Examples](/examples/) - Complete examples diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index d5561f20..182fba78 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -20,6 +20,12 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": [".vitepress/**/*.ts", ".vitepress/**/*.d.ts", ".vitepress/**/*.tsx", ".vitepress/**/*.vue", ".vitepress/**/*.mts"], + "include": [ + ".vitepress/**/*.ts", + ".vitepress/**/*.d.ts", + ".vitepress/**/*.tsx", + ".vitepress/**/*.vue", + ".vitepress/**/*.mts" + ], "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file +} diff --git a/apps/docs/tsconfig.node.json b/apps/docs/tsconfig.node.json index 836f5d95..69900490 100644 --- a/apps/docs/tsconfig.node.json +++ b/apps/docs/tsconfig.node.json @@ -7,4 +7,4 @@ "allowSyntheticDefaultImports": true }, "include": [".vitepress/config.mts"] -} \ No newline at end of file +} diff --git a/apps/perf/.prettierignore b/apps/perf/.prettierignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/apps/perf/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/apps/perf/bootstrap.css b/apps/perf/bootstrap.css index 4cf729e4..14fa6daa 100644 --- a/apps/perf/bootstrap.css +++ b/apps/perf/bootstrap.css @@ -2,5 +2,7156 @@ * Bootstrap v3.3.6 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file + */ /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: 700; +} +dfn { + font-style: italic; +} +h1 { + margin: 0.67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type='button'], +input[type='reset'], +input[type='submit'] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type='checkbox'], +input[type='radio'] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + height: auto; +} +input[type='search'] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: 0.35em 0.625em 0.75em; + margin: 0 2px; + border: 1px solid silver; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: 700; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} /*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + :after, + :before { + color: #000 !important; + text-shadow: none !important; + background: 0 0 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: ' (' attr(href) ')'; + } + abbr[title]:after { + content: ' (' attr(title) ')'; + } + a[href^='javascript:']:after, + a[href^='#']:after { + content: ''; + } + blockquote, + pre { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + img, + tr { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + h2, + h3, + p { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered td, + .table-bordered th { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + src: url(../fonts/glyphicons-halflings-regular.eot); + src: + url(../fonts/glyphicons-halflings-regular.eot?#iefix) + format('embedded-opentype'), + url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'), + url(../fonts/glyphicons-halflings-regular.woff) format('woff'), + url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'), + url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) + format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: '\002a'; +} +.glyphicon-plus:before { + content: '\002b'; +} +.glyphicon-eur:before, +.glyphicon-euro:before { + content: '\20ac'; +} +.glyphicon-minus:before { + content: '\2212'; +} +.glyphicon-cloud:before { + content: '\2601'; +} +.glyphicon-envelope:before { + content: '\2709'; +} +.glyphicon-pencil:before { + content: '\270f'; +} +.glyphicon-glass:before { + content: '\e001'; +} +.glyphicon-music:before { + content: '\e002'; +} +.glyphicon-search:before { + content: '\e003'; +} +.glyphicon-heart:before { + content: '\e005'; +} +.glyphicon-star:before { + content: '\e006'; +} +.glyphicon-star-empty:before { + content: '\e007'; +} +.glyphicon-user:before { + content: '\e008'; +} +.glyphicon-film:before { + content: '\e009'; +} +.glyphicon-th-large:before { + content: '\e010'; +} +.glyphicon-th:before { + content: '\e011'; +} +.glyphicon-th-list:before { + content: '\e012'; +} +.glyphicon-ok:before { + content: '\e013'; +} +.glyphicon-remove:before { + content: '\e014'; +} +.glyphicon-zoom-in:before { + content: '\e015'; +} +.glyphicon-zoom-out:before { + content: '\e016'; +} +.glyphicon-off:before { + content: '\e017'; +} +.glyphicon-signal:before { + content: '\e018'; +} +.glyphicon-cog:before { + content: '\e019'; +} +.glyphicon-trash:before { + content: '\e020'; +} +.glyphicon-home:before { + content: '\e021'; +} +.glyphicon-file:before { + content: '\e022'; +} +.glyphicon-time:before { + content: '\e023'; +} +.glyphicon-road:before { + content: '\e024'; +} +.glyphicon-download-alt:before { + content: '\e025'; +} +.glyphicon-download:before { + content: '\e026'; +} +.glyphicon-upload:before { + content: '\e027'; +} +.glyphicon-inbox:before { + content: '\e028'; +} +.glyphicon-play-circle:before { + content: '\e029'; +} +.glyphicon-repeat:before { + content: '\e030'; +} +.glyphicon-refresh:before { + content: '\e031'; +} +.glyphicon-list-alt:before { + content: '\e032'; +} +.glyphicon-lock:before { + content: '\e033'; +} +.glyphicon-flag:before { + content: '\e034'; +} +.glyphicon-headphones:before { + content: '\e035'; +} +.glyphicon-volume-off:before { + content: '\e036'; +} +.glyphicon-volume-down:before { + content: '\e037'; +} +.glyphicon-volume-up:before { + content: '\e038'; +} +.glyphicon-qrcode:before { + content: '\e039'; +} +.glyphicon-barcode:before { + content: '\e040'; +} +.glyphicon-tag:before { + content: '\e041'; +} +.glyphicon-tags:before { + content: '\e042'; +} +.glyphicon-book:before { + content: '\e043'; +} +.glyphicon-bookmark:before { + content: '\e044'; +} +.glyphicon-print:before { + content: '\e045'; +} +.glyphicon-camera:before { + content: '\e046'; +} +.glyphicon-font:before { + content: '\e047'; +} +.glyphicon-bold:before { + content: '\e048'; +} +.glyphicon-italic:before { + content: '\e049'; +} +.glyphicon-text-height:before { + content: '\e050'; +} +.glyphicon-text-width:before { + content: '\e051'; +} +.glyphicon-align-left:before { + content: '\e052'; +} +.glyphicon-align-center:before { + content: '\e053'; +} +.glyphicon-align-right:before { + content: '\e054'; +} +.glyphicon-align-justify:before { + content: '\e055'; +} +.glyphicon-list:before { + content: '\e056'; +} +.glyphicon-indent-left:before { + content: '\e057'; +} +.glyphicon-indent-right:before { + content: '\e058'; +} +.glyphicon-facetime-video:before { + content: '\e059'; +} +.glyphicon-picture:before { + content: '\e060'; +} +.glyphicon-map-marker:before { + content: '\e062'; +} +.glyphicon-adjust:before { + content: '\e063'; +} +.glyphicon-tint:before { + content: '\e064'; +} +.glyphicon-edit:before { + content: '\e065'; +} +.glyphicon-share:before { + content: '\e066'; +} +.glyphicon-check:before { + content: '\e067'; +} +.glyphicon-move:before { + content: '\e068'; +} +.glyphicon-step-backward:before { + content: '\e069'; +} +.glyphicon-fast-backward:before { + content: '\e070'; +} +.glyphicon-backward:before { + content: '\e071'; +} +.glyphicon-play:before { + content: '\e072'; +} +.glyphicon-pause:before { + content: '\e073'; +} +.glyphicon-stop:before { + content: '\e074'; +} +.glyphicon-forward:before { + content: '\e075'; +} +.glyphicon-fast-forward:before { + content: '\e076'; +} +.glyphicon-step-forward:before { + content: '\e077'; +} +.glyphicon-eject:before { + content: '\e078'; +} +.glyphicon-chevron-left:before { + content: '\e079'; +} +.glyphicon-chevron-right:before { + content: '\e080'; +} +.glyphicon-plus-sign:before { + content: '\e081'; +} +.glyphicon-minus-sign:before { + content: '\e082'; +} +.glyphicon-remove-sign:before { + content: '\e083'; +} +.glyphicon-ok-sign:before { + content: '\e084'; +} +.glyphicon-question-sign:before { + content: '\e085'; +} +.glyphicon-info-sign:before { + content: '\e086'; +} +.glyphicon-screenshot:before { + content: '\e087'; +} +.glyphicon-remove-circle:before { + content: '\e088'; +} +.glyphicon-ok-circle:before { + content: '\e089'; +} +.glyphicon-ban-circle:before { + content: '\e090'; +} +.glyphicon-arrow-left:before { + content: '\e091'; +} +.glyphicon-arrow-right:before { + content: '\e092'; +} +.glyphicon-arrow-up:before { + content: '\e093'; +} +.glyphicon-arrow-down:before { + content: '\e094'; +} +.glyphicon-share-alt:before { + content: '\e095'; +} +.glyphicon-resize-full:before { + content: '\e096'; +} +.glyphicon-resize-small:before { + content: '\e097'; +} +.glyphicon-exclamation-sign:before { + content: '\e101'; +} +.glyphicon-gift:before { + content: '\e102'; +} +.glyphicon-leaf:before { + content: '\e103'; +} +.glyphicon-fire:before { + content: '\e104'; +} +.glyphicon-eye-open:before { + content: '\e105'; +} +.glyphicon-eye-close:before { + content: '\e106'; +} +.glyphicon-warning-sign:before { + content: '\e107'; +} +.glyphicon-plane:before { + content: '\e108'; +} +.glyphicon-calendar:before { + content: '\e109'; +} +.glyphicon-random:before { + content: '\e110'; +} +.glyphicon-comment:before { + content: '\e111'; +} +.glyphicon-magnet:before { + content: '\e112'; +} +.glyphicon-chevron-up:before { + content: '\e113'; +} +.glyphicon-chevron-down:before { + content: '\e114'; +} +.glyphicon-retweet:before { + content: '\e115'; +} +.glyphicon-shopping-cart:before { + content: '\e116'; +} +.glyphicon-folder-close:before { + content: '\e117'; +} +.glyphicon-folder-open:before { + content: '\e118'; +} +.glyphicon-resize-vertical:before { + content: '\e119'; +} +.glyphicon-resize-horizontal:before { + content: '\e120'; +} +.glyphicon-hdd:before { + content: '\e121'; +} +.glyphicon-bullhorn:before { + content: '\e122'; +} +.glyphicon-bell:before { + content: '\e123'; +} +.glyphicon-certificate:before { + content: '\e124'; +} +.glyphicon-thumbs-up:before { + content: '\e125'; +} +.glyphicon-thumbs-down:before { + content: '\e126'; +} +.glyphicon-hand-right:before { + content: '\e127'; +} +.glyphicon-hand-left:before { + content: '\e128'; +} +.glyphicon-hand-up:before { + content: '\e129'; +} +.glyphicon-hand-down:before { + content: '\e130'; +} +.glyphicon-circle-arrow-right:before { + content: '\e131'; +} +.glyphicon-circle-arrow-left:before { + content: '\e132'; +} +.glyphicon-circle-arrow-up:before { + content: '\e133'; +} +.glyphicon-circle-arrow-down:before { + content: '\e134'; +} +.glyphicon-globe:before { + content: '\e135'; +} +.glyphicon-wrench:before { + content: '\e136'; +} +.glyphicon-tasks:before { + content: '\e137'; +} +.glyphicon-filter:before { + content: '\e138'; +} +.glyphicon-briefcase:before { + content: '\e139'; +} +.glyphicon-fullscreen:before { + content: '\e140'; +} +.glyphicon-dashboard:before { + content: '\e141'; +} +.glyphicon-paperclip:before { + content: '\e142'; +} +.glyphicon-heart-empty:before { + content: '\e143'; +} +.glyphicon-link:before { + content: '\e144'; +} +.glyphicon-phone:before { + content: '\e145'; +} +.glyphicon-pushpin:before { + content: '\e146'; +} +.glyphicon-usd:before { + content: '\e148'; +} +.glyphicon-gbp:before { + content: '\e149'; +} +.glyphicon-sort:before { + content: '\e150'; +} +.glyphicon-sort-by-alphabet:before { + content: '\e151'; +} +.glyphicon-sort-by-alphabet-alt:before { + content: '\e152'; +} +.glyphicon-sort-by-order:before { + content: '\e153'; +} +.glyphicon-sort-by-order-alt:before { + content: '\e154'; +} +.glyphicon-sort-by-attributes:before { + content: '\e155'; +} +.glyphicon-sort-by-attributes-alt:before { + content: '\e156'; +} +.glyphicon-unchecked:before { + content: '\e157'; +} +.glyphicon-expand:before { + content: '\e158'; +} +.glyphicon-collapse-down:before { + content: '\e159'; +} +.glyphicon-collapse-up:before { + content: '\e160'; +} +.glyphicon-log-in:before { + content: '\e161'; +} +.glyphicon-flash:before { + content: '\e162'; +} +.glyphicon-log-out:before { + content: '\e163'; +} +.glyphicon-new-window:before { + content: '\e164'; +} +.glyphicon-record:before { + content: '\e165'; +} +.glyphicon-save:before { + content: '\e166'; +} +.glyphicon-open:before { + content: '\e167'; +} +.glyphicon-saved:before { + content: '\e168'; +} +.glyphicon-import:before { + content: '\e169'; +} +.glyphicon-export:before { + content: '\e170'; +} +.glyphicon-send:before { + content: '\e171'; +} +.glyphicon-floppy-disk:before { + content: '\e172'; +} +.glyphicon-floppy-saved:before { + content: '\e173'; +} +.glyphicon-floppy-remove:before { + content: '\e174'; +} +.glyphicon-floppy-save:before { + content: '\e175'; +} +.glyphicon-floppy-open:before { + content: '\e176'; +} +.glyphicon-credit-card:before { + content: '\e177'; +} +.glyphicon-transfer:before { + content: '\e178'; +} +.glyphicon-cutlery:before { + content: '\e179'; +} +.glyphicon-header:before { + content: '\e180'; +} +.glyphicon-compressed:before { + content: '\e181'; +} +.glyphicon-earphone:before { + content: '\e182'; +} +.glyphicon-phone-alt:before { + content: '\e183'; +} +.glyphicon-tower:before { + content: '\e184'; +} +.glyphicon-stats:before { + content: '\e185'; +} +.glyphicon-sd-video:before { + content: '\e186'; +} +.glyphicon-hd-video:before { + content: '\e187'; +} +.glyphicon-subtitles:before { + content: '\e188'; +} +.glyphicon-sound-stereo:before { + content: '\e189'; +} +.glyphicon-sound-dolby:before { + content: '\e190'; +} +.glyphicon-sound-5-1:before { + content: '\e191'; +} +.glyphicon-sound-6-1:before { + content: '\e192'; +} +.glyphicon-sound-7-1:before { + content: '\e193'; +} +.glyphicon-copyright-mark:before { + content: '\e194'; +} +.glyphicon-registration-mark:before { + content: '\e195'; +} +.glyphicon-cloud-download:before { + content: '\e197'; +} +.glyphicon-cloud-upload:before { + content: '\e198'; +} +.glyphicon-tree-conifer:before { + content: '\e199'; +} +.glyphicon-tree-deciduous:before { + content: '\e200'; +} +.glyphicon-cd:before { + content: '\e201'; +} +.glyphicon-save-file:before { + content: '\e202'; +} +.glyphicon-open-file:before { + content: '\e203'; +} +.glyphicon-level-up:before { + content: '\e204'; +} +.glyphicon-copy:before { + content: '\e205'; +} +.glyphicon-paste:before { + content: '\e206'; +} +.glyphicon-alert:before { + content: '\e209'; +} +.glyphicon-equalizer:before { + content: '\e210'; +} +.glyphicon-king:before { + content: '\e211'; +} +.glyphicon-queen:before { + content: '\e212'; +} +.glyphicon-pawn:before { + content: '\e213'; +} +.glyphicon-bishop:before { + content: '\e214'; +} +.glyphicon-knight:before { + content: '\e215'; +} +.glyphicon-baby-formula:before { + content: '\e216'; +} +.glyphicon-tent:before { + content: '\26fa'; +} +.glyphicon-blackboard:before { + content: '\e218'; +} +.glyphicon-bed:before { + content: '\e219'; +} +.glyphicon-apple:before { + content: '\f8ff'; +} +.glyphicon-erase:before { + content: '\e221'; +} +.glyphicon-hourglass:before { + content: '\231b'; +} +.glyphicon-lamp:before { + content: '\e223'; +} +.glyphicon-duplicate:before { + content: '\e224'; +} +.glyphicon-piggy-bank:before { + content: '\e225'; +} +.glyphicon-scissors:before { + content: '\e226'; +} +.glyphicon-bitcoin:before { + content: '\e227'; +} +.glyphicon-btc:before { + content: '\e227'; +} +.glyphicon-xbt:before { + content: '\e227'; +} +.glyphicon-yen:before { + content: '\00a5'; +} +.glyphicon-jpy:before { + content: '\00a5'; +} +.glyphicon-ruble:before { + content: '\20bd'; +} +.glyphicon-rub:before { + content: '\20bd'; +} +.glyphicon-scale:before { + content: '\e230'; +} +.glyphicon-ice-lolly:before { + content: '\e231'; +} +.glyphicon-ice-lolly-tasted:before { + content: '\e232'; +} +.glyphicon-education:before { + content: '\e233'; +} +.glyphicon-option-horizontal:before { + content: '\e234'; +} +.glyphicon-option-vertical:before { + content: '\e235'; +} +.glyphicon-menu-hamburger:before { + content: '\e236'; +} +.glyphicon-modal-window:before { + content: '\e237'; +} +.glyphicon-oil:before { + content: '\e238'; +} +.glyphicon-grain:before { + content: '\e239'; +} +.glyphicon-sunglasses:before { + content: '\e240'; +} +.glyphicon-text-size:before { + content: '\e241'; +} +.glyphicon-text-color:before { + content: '\e242'; +} +.glyphicon-text-background:before { + content: '\e243'; +} +.glyphicon-object-align-top:before { + content: '\e244'; +} +.glyphicon-object-align-bottom:before { + content: '\e245'; +} +.glyphicon-object-align-horizontal:before { + content: '\e246'; +} +.glyphicon-object-align-left:before { + content: '\e247'; +} +.glyphicon-object-align-vertical:before { + content: '\e248'; +} +.glyphicon-object-align-right:before { + content: '\e249'; +} +.glyphicon-triangle-right:before { + content: '\e250'; +} +.glyphicon-triangle-left:before { + content: '\e251'; +} +.glyphicon-triangle-bottom:before { + content: '\e252'; +} +.glyphicon-triangle-top:before { + content: '\e253'; +} +.glyphicon-console:before { + content: '\e254'; +} +.glyphicon-superscript:before { + content: '\e255'; +} +.glyphicon-subscript:before { + content: '\e256'; +} +.glyphicon-menu-left:before { + content: '\e257'; +} +.glyphicon-menu-right:before { + content: '\e258'; +} +.glyphicon-menu-down:before { + content: '\e259'; +} +.glyphicon-menu-up:before { + content: '\e260'; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +:after, +:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:focus, +a:hover { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.carousel-inner > .item > a > img, +.carousel-inner > .item > img, +.img-responsive, +.thumbnail a > img, +.thumbnail > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role='button'] { + cursor: pointer; +} +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +.h1 .small, +.h1 small, +.h2 .small, +.h2 small, +.h3 .small, +.h3 small, +.h4 .small, +.h4 small, +.h5 .small, +.h5 small, +.h6 .small, +.h6 small, +h1 .small, +h1 small, +h2 .small, +h2 small, +h3 .small, +h3 small, +h4 .small, +h4 small, +h5 .small, +h5 small, +h6 .small, +h6 small { + font-weight: 400; + line-height: 1; + color: #777; +} +.h1, +.h2, +.h3, +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +.h1 .small, +.h1 small, +.h2 .small, +.h2 small, +.h3 .small, +.h3 small, +h1 .small, +h1 small, +h2 .small, +h2 small, +h3 .small, +h3 small { + font-size: 65%; +} +.h4, +.h5, +.h6, +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +.h4 .small, +.h4 small, +.h5 .small, +.h5 small, +.h6 .small, +.h6 small, +h4 .small, +h4 small, +h5 .small, +h5 small, +h6 .small, +h6 small { + font-size: 75%; +} +.h1, +h1 { + font-size: 36px; +} +.h2, +h2 { + font-size: 30px; +} +.h3, +h3 { + font-size: 24px; +} +.h4, +h4 { + font-size: 18px; +} +.h5, +h5 { + font-size: 14px; +} +.h6, +h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +.small, +small { + font-size: 85%; +} +.mark, +mark { + padding: 0.2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:focus, +a.text-primary:hover { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:focus, +a.text-success:hover { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:focus, +a.text-info:hover { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:focus, +a.text-warning:hover { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:focus, +a.text-danger:hover { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:focus, +a.bg-primary:hover { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:focus, +a.bg-success:hover { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:focus, +a.bg-info:hover { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:focus, +a.bg-warning:hover { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:focus, +a.bg-danger:hover { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ol, +ul { + margin-top: 0; + margin-bottom: 10px; +} +ol ol, +ol ul, +ul ol, +ul ul { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dd, +dt { + line-height: 1.42857143; +} +dt { + font-weight: 700; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[data-original-title], +abbr[title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote ol:last-child, +blockquote p:last-child, +blockquote ul:last-child { + margin-bottom: 0; +} +blockquote .small, +blockquote footer, +blockquote small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote .small:before, +blockquote footer:before, +blockquote small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse .small:before, +.blockquote-reverse footer:before, +.blockquote-reverse small:before, +blockquote.pull-right .small:before, +blockquote.pull-right footer:before, +blockquote.pull-right small:before { + content: ''; +} +.blockquote-reverse .small:after, +.blockquote-reverse footer:after, +.blockquote-reverse small:after, +blockquote.pull-right .small:after, +blockquote.pull-right footer:after, +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-lg-1, +.col-lg-10, +.col-lg-11, +.col-lg-12, +.col-lg-2, +.col-lg-3, +.col-lg-4, +.col-lg-5, +.col-lg-6, +.col-lg-7, +.col-lg-8, +.col-lg-9, +.col-md-1, +.col-md-10, +.col-md-11, +.col-md-12, +.col-md-2, +.col-md-3, +.col-md-4, +.col-md-5, +.col-md-6, +.col-md-7, +.col-md-8, +.col-md-9, +.col-sm-1, +.col-sm-10, +.col-sm-11, +.col-sm-12, +.col-sm-2, +.col-sm-3, +.col-sm-4, +.col-sm-5, +.col-sm-6, +.col-sm-7, +.col-sm-8, +.col-sm-9, +.col-xs-1, +.col-xs-10, +.col-xs-11, +.col-xs-12, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, +.col-xs-10, +.col-xs-11, +.col-xs-12, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, + .col-sm-10, + .col-sm-11, + .col-sm-12, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, + .col-md-10, + .col-md-11, + .col-md-12, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, + .col-lg-10, + .col-lg-11, + .col-lg-12, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > tbody > tr > td, +.table > tbody > tr > th, +.table > tfoot > tr > td, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > thead > tr > th { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > td, +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > td, +.table > thead:first-child > tr:first-child > th { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > tbody > tr > td, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > td, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > thead > tr > th { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > tbody > tr > td, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > td, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > thead > tr > th { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > td, +.table-bordered > thead > tr > th { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*='col-'] { + position: static; + display: table-column; + float: none; +} +table td[class*='col-'], +table th[class*='col-'] { + position: static; + display: table-cell; + float: none; +} +.table > tbody > tr.active > td, +.table > tbody > tr.active > th, +.table > tbody > tr > td.active, +.table > tbody > tr > th.active, +.table > tfoot > tr.active > td, +.table > tfoot > tr.active > th, +.table > tfoot > tr > td.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > thead > tr.active > th, +.table > thead > tr > td.active, +.table > thead > tr > th.active { + background-color: #f5f5f5; +} +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr.active:hover > th, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover { + background-color: #e8e8e8; +} +.table > tbody > tr.success > td, +.table > tbody > tr.success > th, +.table > tbody > tr > td.success, +.table > tbody > tr > th.success, +.table > tfoot > tr.success > td, +.table > tfoot > tr.success > th, +.table > tfoot > tr > td.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > thead > tr.success > th, +.table > thead > tr > td.success, +.table > thead > tr > th.success { + background-color: #dff0d8; +} +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr.success:hover > th, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover { + background-color: #d0e9c6; +} +.table > tbody > tr.info > td, +.table > tbody > tr.info > th, +.table > tbody > tr > td.info, +.table > tbody > tr > th.info, +.table > tfoot > tr.info > td, +.table > tfoot > tr.info > th, +.table > tfoot > tr > td.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > thead > tr.info > th, +.table > thead > tr > td.info, +.table > thead > tr > th.info { + background-color: #d9edf7; +} +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr.info:hover > th, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover { + background-color: #c4e3f3; +} +.table > tbody > tr.warning > td, +.table > tbody > tr.warning > th, +.table > tbody > tr > td.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr.warning > td, +.table > tfoot > tr.warning > th, +.table > tfoot > tr > td.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > thead > tr.warning > th, +.table > thead > tr > td.warning, +.table > thead > tr > th.warning { + background-color: #fcf8e3; +} +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr.warning:hover > th, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover { + background-color: #faf2cc; +} +.table > tbody > tr.danger > td, +.table > tbody > tr.danger > th, +.table > tbody > tr > td.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr.danger > td, +.table > tfoot > tr.danger > th, +.table > tfoot > tr > td.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > thead > tr.danger > th, +.table > thead > tr > td.danger, +.table > thead > tr > th.danger { + background-color: #f2dede; +} +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr.danger:hover > th, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover { + background-color: #ebcccc; +} +.table-responsive { + min-height: 0.01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > td, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > thead > tr > th { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > thead > tr > th:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > thead > tr > th:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: 700; +} +input[type='search'] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type='checkbox'], +input[type='radio'] { + margin: 4px 0 0; + margin-top: 1px\9; + line-height: normal; +} +input[type='file'] { + display: block; +} +input[type='range'] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type='file']:focus, +input[type='checkbox']:focus, +input[type='radio']:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: + border-color ease-in-out 0.15s, + -webkit-box-shadow ease-in-out 0.15s; + -o-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 8px rgba(102, 175, 233, 0.6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eee; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +input[type='search'] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type='date'].form-control, + input[type='time'].form-control, + input[type='datetime-local'].form-control, + input[type='month'].form-control { + line-height: 34px; + } + .input-group-sm input[type='date'], + .input-group-sm input[type='time'], + .input-group-sm input[type='datetime-local'], + .input-group-sm input[type='month'], + input[type='date'].input-sm, + input[type='time'].input-sm, + input[type='datetime-local'].input-sm, + input[type='month'].input-sm { + line-height: 30px; + } + .input-group-lg input[type='date'], + .input-group-lg input[type='time'], + .input-group-lg input[type='datetime-local'], + .input-group-lg input[type='month'], + input[type='date'].input-lg, + input[type='time'].input-lg, + input[type='datetime-local'].input-lg, + input[type='month'].input-lg { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.checkbox, +.radio { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.checkbox label, +.radio label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + cursor: pointer; +} +.checkbox input[type='checkbox'], +.checkbox-inline input[type='checkbox'], +.radio input[type='radio'], +.radio-inline input[type='radio'] { + position: absolute; + margin-top: 4px\9; + margin-left: -20px; +} +.checkbox + .checkbox, +.radio + .radio { + margin-top: -5px; +} +.checkbox-inline, +.radio-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + vertical-align: middle; + cursor: pointer; +} +.checkbox-inline + .checkbox-inline, +.radio-inline + .radio-inline { + margin-top: 0; + margin-left: 10px; +} +fieldset[disabled] input[type='checkbox'], +fieldset[disabled] input[type='radio'], +input[type='checkbox'].disabled, +input[type='checkbox'][disabled], +input[type='radio'].disabled, +input[type='radio'][disabled] { + cursor: not-allowed; +} +.checkbox-inline.disabled, +.radio-inline.disabled, +fieldset[disabled] .checkbox-inline, +fieldset[disabled] .radio-inline { + cursor: not-allowed; +} +.checkbox.disabled label, +.radio.disabled label, +fieldset[disabled] .checkbox label, +fieldset[disabled] .radio label { + cursor: not-allowed; +} +.form-control-static { + min-height: 34px; + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +select[multiple].input-sm, +textarea.input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm select[multiple].form-control, +.form-group-sm textarea.form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +select[multiple].input-lg, +textarea.input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} +.form-group-lg select[multiple].form-control, +.form-group-lg textarea.form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.form-group-lg .form-control + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.form-group-sm .form-control + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .checkbox, +.has-success .checkbox-inline, +.has-success .control-label, +.has-success .help-block, +.has-success .radio, +.has-success .radio-inline, +.has-success.checkbox label, +.has-success.checkbox-inline label, +.has-success.radio label, +.has-success.radio-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #67b168; + box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .checkbox, +.has-warning .checkbox-inline, +.has-warning .control-label, +.has-warning .help-block, +.has-warning .radio, +.has-warning .radio-inline, +.has-warning.checkbox label, +.has-warning.checkbox-inline label, +.has-warning.radio label, +.has-warning.radio-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #c0a16b; + box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .checkbox, +.has-error .checkbox-inline, +.has-error .control-label, +.has-error .help-block, +.has-error .radio, +.has-error .radio-inline, +.has-error.checkbox label, +.has-error.checkbox-inline label, +.has-error.radio label, +.has-error.radio-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #ce8483; + box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .form-control, + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .checkbox, + .form-inline .radio { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .checkbox label, + .form-inline .radio label { + padding-left: 0; + } + .form-inline .checkbox input[type='checkbox'], + .form-inline .radio input[type='radio'] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .checkbox, +.form-horizontal .checkbox-inline, +.form-horizontal .radio, +.form-horizontal .radio-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .checkbox, +.form-horizontal .radio { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 11px; + font-size: 18px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn.active.focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn:active:focus, +.btn:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.focus, +.btn:focus, +.btn:hover { + color: #333; + text-decoration: none; +} +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: 0.65; +} +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default.focus, +.btn-default:focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default.active, +.btn-default:active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default.active.focus, +.btn-default.active:focus, +.btn-default.active:hover, +.btn-default:active.focus, +.btn-default:active:focus, +.btn-default:active:hover, +.open > .dropdown-toggle.btn-default.focus, +.open > .dropdown-toggle.btn-default:focus, +.open > .dropdown-toggle.btn-default:hover { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.btn-default.active, +.btn-default:active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled.focus, +.btn-default.disabled:focus, +.btn-default.disabled:hover, +.btn-default[disabled].focus, +.btn-default[disabled]:focus, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default.focus, +fieldset[disabled] .btn-default:focus, +fieldset[disabled] .btn-default:hover { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary.focus, +.btn-primary:focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary.active, +.btn-primary:active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary.active.focus, +.btn-primary.active:focus, +.btn-primary.active:hover, +.btn-primary:active.focus, +.btn-primary:active:focus, +.btn-primary:active:hover, +.open > .dropdown-toggle.btn-primary.focus, +.open > .dropdown-toggle.btn-primary:focus, +.open > .dropdown-toggle.btn-primary:hover { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} +.btn-primary.active, +.btn-primary:active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled.focus, +.btn-primary.disabled:focus, +.btn-primary.disabled:hover, +.btn-primary[disabled].focus, +.btn-primary[disabled]:focus, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary.focus, +fieldset[disabled] .btn-primary:focus, +fieldset[disabled] .btn-primary:hover { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success.focus, +.btn-success:focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success.active, +.btn-success:active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success.active.focus, +.btn-success.active:focus, +.btn-success.active:hover, +.btn-success:active.focus, +.btn-success:active:focus, +.btn-success:active:hover, +.open > .dropdown-toggle.btn-success.focus, +.open > .dropdown-toggle.btn-success:focus, +.open > .dropdown-toggle.btn-success:hover { + color: #fff; + background-color: #398439; + border-color: #255625; +} +.btn-success.active, +.btn-success:active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled.focus, +.btn-success.disabled:focus, +.btn-success.disabled:hover, +.btn-success[disabled].focus, +.btn-success[disabled]:focus, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success.focus, +fieldset[disabled] .btn-success:focus, +fieldset[disabled] .btn-success:hover { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info.focus, +.btn-info:focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info.active, +.btn-info:active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info.active.focus, +.btn-info.active:focus, +.btn-info.active:hover, +.btn-info:active.focus, +.btn-info:active:focus, +.btn-info:active:hover, +.open > .dropdown-toggle.btn-info.focus, +.open > .dropdown-toggle.btn-info:focus, +.open > .dropdown-toggle.btn-info:hover { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} +.btn-info.active, +.btn-info:active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled.focus, +.btn-info.disabled:focus, +.btn-info.disabled:hover, +.btn-info[disabled].focus, +.btn-info[disabled]:focus, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info.focus, +fieldset[disabled] .btn-info:focus, +fieldset[disabled] .btn-info:hover { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning.focus, +.btn-warning:focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning.active, +.btn-warning:active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning.active.focus, +.btn-warning.active:focus, +.btn-warning.active:hover, +.btn-warning:active.focus, +.btn-warning:active:focus, +.btn-warning:active:hover, +.open > .dropdown-toggle.btn-warning.focus, +.open > .dropdown-toggle.btn-warning:focus, +.open > .dropdown-toggle.btn-warning:hover { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} +.btn-warning.active, +.btn-warning:active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled.focus, +.btn-warning.disabled:focus, +.btn-warning.disabled:hover, +.btn-warning[disabled].focus, +.btn-warning[disabled]:focus, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning.focus, +fieldset[disabled] .btn-warning:focus, +fieldset[disabled] .btn-warning:hover { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger.focus, +.btn-danger:focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger.active, +.btn-danger:active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger.active.focus, +.btn-danger.active:focus, +.btn-danger.active:hover, +.btn-danger:active.focus, +.btn-danger:active:focus, +.btn-danger:active:hover, +.open > .dropdown-toggle.btn-danger.focus, +.open > .dropdown-toggle.btn-danger:focus, +.open > .dropdown-toggle.btn-danger:hover { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} +.btn-danger.active, +.btn-danger:active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled.focus, +.btn-danger.disabled:focus, +.btn-danger.disabled:hover, +.btn-danger[disabled].focus, +.btn-danger[disabled]:focus, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger.focus, +fieldset[disabled] .btn-danger:focus, +fieldset[disabled] .btn-danger:hover { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: 400; + color: #337ab7; + border-radius: 0; +} +.btn-link, +.btn-link.active, +.btn-link:active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:active, +.btn-link:focus, +.btn-link:hover { + border-color: transparent; +} +.btn-link:focus, +.btn-link:hover { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:focus, +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:focus, +fieldset[disabled] .btn-link:hover { + color: #777; + text-decoration: none; +} +.btn-group-lg > .btn, +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-group-sm > .btn, +.btn-sm { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-group-xs > .btn, +.btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type='button'].btn-block, +input[type='reset'].btn-block, +input[type='submit'].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: 0.35s; + -o-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid\9; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropdown, +.dropup { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: 400; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:focus, +.dropdown-menu > li > a:hover { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:focus, +.dropdown-menu > .active > a:hover { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:focus, +.dropdown-menu > .disabled > a:hover { + color: #777; +} +.dropdown-menu > .disabled > a:focus, +.dropdown-menu > .disabled > a:hover { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ''; + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid\9; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group-vertical > .btn, +.btn-group > .btn { + position: relative; + float: left; +} +.btn-group-vertical > .btn.active, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:hover, +.btn-group > .btn.active, +.btn-group > .btn:active, +.btn-group > .btn:focus, +.btn-group > .btn:hover { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical + > .btn-group:first-child:not(:last-child) + > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical + > .btn-group:last-child:not(:first-child) + > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle='buttons'] > .btn input[type='checkbox'], +[data-toggle='buttons'] > .btn input[type='radio'], +[data-toggle='buttons'] > .btn-group > .btn input[type='checkbox'], +[data-toggle='buttons'] > .btn-group > .btn input[type='radio'] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*='col-'] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group .form-control:focus { + z-index: 3; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn, +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn, +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group .form-control, +.input-group-addon, +.input-group-btn { + display: table-cell; +} +.input-group .form-control:not(:first-child):not(:last-child), +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: 400; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type='checkbox'], +.input-group-addon input[type='radio'] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:active, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:hover { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:focus, +.nav > li > a:hover { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:focus, +.nav > li.disabled > a:hover { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:focus, +.nav .open > a:hover { + background-color: #eee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:focus, +.nav-tabs > li.active > a:hover { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:focus, +.nav-tabs.nav-justified > .active > a:hover { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:focus, + .nav-tabs.nav-justified > .active > a:hover { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:focus, +.nav-pills > li.active > a:hover { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:focus, +.nav-tabs-justified > .active > a:hover { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:focus, + .nav-tabs-justified > .active > a:hover { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-bottom .navbar-collapse, + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-bottom .navbar-collapse, +.navbar-fixed-top .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-bottom .navbar-collapse, + .navbar-fixed-top .navbar-collapse { + max-height: 200px; + } +} +.container-fluid > .navbar-collapse, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container > .navbar-header { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container-fluid > .navbar-collapse, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container > .navbar-header { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-bottom, +.navbar-fixed-top { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-bottom, + .navbar-fixed-top { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:focus, +.navbar-brand:hover { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu .dropdown-header, + .navbar-nav .open .dropdown-menu > li > a { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:focus, + .navbar-nav .open .dropdown-menu > li > a:hover { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 1px 0 rgba(255, 255, 255, 0.1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .form-control, + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .checkbox, + .navbar-form .radio { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .checkbox label, + .navbar-form .radio label { + padding-left: 0; + } + .navbar-form .checkbox input[type='checkbox'], + .navbar-form .radio input[type='radio'] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:focus, +.navbar-default .navbar-brand:hover { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:focus, +.navbar-default .navbar-nav > li > a:hover { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:focus, +.navbar-default .navbar-nav > .active > a:hover { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:focus, +.navbar-default .navbar-nav > .disabled > a:hover { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:focus, +.navbar-default .navbar-toggle:hover { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:focus, +.navbar-default .navbar-nav > .open > a:hover { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:focus, +.navbar-default .btn-link:hover { + color: #333; +} +.navbar-default .btn-link[disabled]:focus, +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:focus, +fieldset[disabled] .navbar-default .btn-link:hover { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:focus, +.navbar-inverse .navbar-brand:hover { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:focus, +.navbar-inverse .navbar-nav > li > a:hover { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:focus, +.navbar-inverse .navbar-nav > .active > a:hover { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:focus, +.navbar-inverse .navbar-nav > .disabled > a:hover { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:focus, +.navbar-inverse .navbar-toggle:hover { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:focus, +.navbar-inverse .navbar-nav > .open > a:hover { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:focus, +.navbar-inverse .btn-link:hover { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:focus, +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:focus, +fieldset[disabled] .navbar-inverse .btn-link:hover { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: '/\00a0'; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:focus, +.pagination > li > a:hover, +.pagination > li > span:focus, +.pagination > li > span:hover { + z-index: 2; + color: #23527c; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > a:focus, +.pagination > .active > a:hover, +.pagination > .active > span, +.pagination > .active > span:focus, +.pagination > .active > span:hover { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > a, +.pagination > .disabled > a:focus, +.pagination > .disabled > a:hover, +.pagination > .disabled > span, +.pagination > .disabled > span:focus, +.pagination > .disabled > span:hover { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:focus, +.pager li > a:hover { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:focus, +.pager .disabled > a:hover, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; +} +a.label:focus, +a.label:hover { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777; +} +.label-default[href]:focus, +.label-default[href]:hover { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:focus, +.label-primary[href]:hover { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:focus, +.label-success[href]:hover { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:focus, +.label-info[href]:hover { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:focus, +.label-warning[href]:hover { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:focus, +.label-danger[href]:hover { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-group-xs > .btn .badge, +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} +a.badge:focus, +a.badge:hover { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron .h1, +.jumbotron h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + padding-right: 15px; + padding-left: 15px; + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron .h1, + .jumbotron h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} +.thumbnail a > img, +.thumbnail > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail.active, +a.thumbnail:focus, +a.thumbnail:hover { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: 700; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-bar-striped, +.progress-striped .progress-bar { + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: -o-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress-bar.active, +.progress.active .progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: -o-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: -o-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: -o-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: -o-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-body, +.media-left, +.media-right { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item, +button.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:focus, +a.list-group-item:hover, +button.list-group-item:focus, +button.list-group-item:hover { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +button.list-group-item { + width: 100%; + text-align: left; +} +.list-group-item.disabled, +.list-group-item.disabled:focus, +.list-group-item.disabled:hover { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:focus, +.list-group-item.active:hover { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:focus .list-group-item-text, +.list-group-item.active:hover .list-group-item-text { + color: #c7ddef; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:focus, +a.list-group-item-success:hover, +button.list-group-item-success:focus, +button.list-group-item-success:hover { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +a.list-group-item-success.active:focus, +a.list-group-item-success.active:hover, +button.list-group-item-success.active, +button.list-group-item-success.active:focus, +button.list-group-item-success.active:hover { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:focus, +a.list-group-item-info:hover, +button.list-group-item-info:focus, +button.list-group-item-info:hover { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:focus, +a.list-group-item-info.active:hover, +button.list-group-item-info.active, +button.list-group-item-info.active:focus, +button.list-group-item-info.active:hover { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:focus, +a.list-group-item-warning:hover, +button.list-group-item-warning:focus, +button.list-group-item-warning:hover { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:focus, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active, +button.list-group-item-warning.active:focus, +button.list-group-item-warning.active:hover { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:focus, +a.list-group-item-danger:hover, +button.list-group-item-danger:focus, +button.list-group-item-danger:hover { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:focus, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active, +button.list-group-item-danger.active:focus, +button.list-group-item-danger.active:hover { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > .small, +.panel-title > .small > a, +.panel-title > a, +.panel-title > small, +.panel-title > small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel + > .panel-collapse + > .list-group:first-child + .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel + > .panel-heading + + .panel-collapse + > .list-group + .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .panel-collapse > .table, +.panel > .table, +.panel > .table-responsive > .table { + margin-bottom: 0; +} +.panel > .panel-collapse > .table caption, +.panel > .table caption, +.panel > .table-responsive > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table-responsive:first-child > .table:first-child, +.panel > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel + > .table-responsive:first-child + > .table:first-child + > tbody:first-child + > tr:first-child, +.panel + > .table-responsive:first-child + > .table:first-child + > thead:first-child + > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel + > .table-responsive:first-child + > .table:first-child + > tbody:first-child + > tr:first-child + td:first-child, +.panel + > .table-responsive:first-child + > .table:first-child + > tbody:first-child + > tr:first-child + th:first-child, +.panel + > .table-responsive:first-child + > .table:first-child + > thead:first-child + > tr:first-child + td:first-child, +.panel + > .table-responsive:first-child + > .table:first-child + > thead:first-child + > tr:first-child + th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel + > .table:first-child + > thead:first-child + > tr:first-child + th:first-child { + border-top-left-radius: 3px; +} +.panel + > .table-responsive:first-child + > .table:first-child + > tbody:first-child + > tr:first-child + td:last-child, +.panel + > .table-responsive:first-child + > .table:first-child + > tbody:first-child + > tr:first-child + th:last-child, +.panel + > .table-responsive:first-child + > .table:first-child + > thead:first-child + > tr:first-child + td:last-child, +.panel + > .table-responsive:first-child + > .table:first-child + > thead:first-child + > tr:first-child + th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table-responsive:last-child > .table:last-child, +.panel > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel + > .table-responsive:last-child + > .table:last-child + > tbody:last-child + > tr:last-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tfoot:last-child + > tr:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel + > .table-responsive:last-child + > .table:last-child + > tbody:last-child + > tr:last-child + td:first-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tbody:last-child + > tr:last-child + th:first-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tfoot:last-child + > tr:last-child + td:first-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tfoot:last-child + > tr:last-child + th:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel + > .table-responsive:last-child + > .table:last-child + > tbody:last-child + > tr:last-child + td:last-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tbody:last-child + > tr:last-child + th:last-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tfoot:last-child + > tr:last-child + td:last-child, +.panel + > .table-responsive:last-child + > .table:last-child + > tfoot:last-child + > tr:last-child + th:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child td, +.panel > .table > tbody:first-child > tr:first-child th { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child { + border-left: 0; +} +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child { + border-right: 0; +} +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .list-group, +.panel-group .panel-heading + .panel-collapse > .panel-body { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive embed, +.embed-responsive iframe, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: 0.2; +} +.close:focus, +.close:hover { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: 0.5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: 0.5; +} +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + filter: alpha(opacity=0); + opacity: 0; + line-break: auto; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: 0.9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + line-break: auto; +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ''; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: ' '; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: ' '; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: ' '; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: ' '; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel-inner > .item > a > img, +.carousel-inner > .item > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.active.right, + .carousel-inner > .item.next { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.active.left, + .carousel-inner > .item.prev { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.active, + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + background-color: rgba(0, 0, 0, 0); + filter: alpha(opacity=50); + opacity: 0.5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient( + left, + rgba(0, 0, 0, 0.5) 0, + rgba(0, 0, 0, 0.0001) 100% + ); + background-image: -o-linear-gradient( + left, + rgba(0, 0, 0, 0.5) 0, + rgba(0, 0, 0, 0.0001) 100% + ); + background-image: -webkit-gradient( + linear, + left top, + right top, + from(rgba(0, 0, 0, 0.5)), + to(rgba(0, 0, 0, 0.0001)) + ); + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.5) 0, + rgba(0, 0, 0, 0.0001) 100% + ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient( + left, + rgba(0, 0, 0, 0.0001) 0, + rgba(0, 0, 0, 0.5) 100% + ); + background-image: -o-linear-gradient( + left, + rgba(0, 0, 0, 0.0001) 0, + rgba(0, 0, 0, 0.5) 100% + ); + background-image: -webkit-gradient( + linear, + left top, + right top, + from(rgba(0, 0, 0, 0.0001)), + to(rgba(0, 0, 0, 0.5)) + ); + background-image: linear-gradient( + to right, + rgba(0, 0, 0, 0.0001) 0, + rgba(0, 0, 0, 0.5) 100% + ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:focus, +.carousel-control:hover { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: 0.9; +} +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right, +.carousel-control .icon-next, +.carousel-control .icon-prev { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -10px; +} +.carousel-control .glyphicon-chevron-left, +.carousel-control .icon-prev { + left: 50%; + margin-left: -10px; +} +.carousel-control .glyphicon-chevron-right, +.carousel-control .icon-next { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-next, +.carousel-control .icon-prev { + width: 20px; + height: 20px; + font-family: serif; + line-height: 1; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000\9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next, + .carousel-control .icon-prev { + width: 30px; + height: 30px; + margin-top: -10px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -10px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -10px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.btn-group-vertical > .btn-group:after, +.btn-group-vertical > .btn-group:before, +.btn-toolbar:after, +.btn-toolbar:before, +.clearfix:after, +.clearfix:before, +.container-fluid:after, +.container-fluid:before, +.container:after, +.container:before, +.dl-horizontal dd:after, +.dl-horizontal dd:before, +.form-horizontal .form-group:after, +.form-horizontal .form-group:before, +.modal-footer:after, +.modal-footer:before, +.modal-header:after, +.modal-header:before, +.nav:after, +.nav:before, +.navbar-collapse:after, +.navbar-collapse:before, +.navbar-header:after, +.navbar-header:before, +.navbar:after, +.navbar:before, +.pager:after, +.pager:before, +.panel-body:after, +.panel-body:before, +.row:after, +.row:before { + display: table; + content: ' '; +} +.btn-group-vertical > .btn-group:after, +.btn-toolbar:after, +.clearfix:after, +.container-fluid:after, +.container:after, +.dl-horizontal dd:after, +.form-horizontal .form-group:after, +.modal-footer:after, +.modal-header:after, +.nav:after, +.navbar-collapse:after, +.navbar-header:after, +.navbar:after, +.pager:after, +.panel-body:after, +.row:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-lg, +.visible-md, +.visible-sm, +.visible-xs { + display: none !important; +} +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + td.visible-xs, + th.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + td.visible-sm, + th.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + td.visible-md, + th.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + td.visible-lg, + th.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + td.visible-print, + th.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap.min.css.map */ diff --git a/apps/perf/index.html b/apps/perf/index.html index 0cda5079..f245ef70 100644 --- a/apps/perf/index.html +++ b/apps/perf/index.html @@ -1,12 +1,12 @@ - + - - - React + Blac Performance - - - -
    - - + + + React + Blac Performance + + + +
    + + diff --git a/apps/perf/main.css b/apps/perf/main.css index d3895a31..2872c9e8 100644 --- a/apps/perf/main.css +++ b/apps/perf/main.css @@ -1,26 +1,26 @@ body { - padding: 10px 0 0 0; - margin: 0; - overflow-y: scroll; + padding: 10px 0 0 0; + margin: 0; + overflow-y: scroll; } #duration { - padding-top: 0px; + padding-top: 0px; } .jumbotron { - padding-top:10px; - padding-bottom:10px; + padding-top: 10px; + padding-bottom: 10px; } .test-data a { - display: block; + display: block; } .preloadicon { - position: absolute; - top:-20px; - left:-20px; + position: absolute; + top: -20px; + left: -20px; } .col-sm-6.smallpad { - padding: 5px; + padding: 5px; } .jumbotron .row h1 { - font-size: 40px; -} \ No newline at end of file + font-size: 40px; +} diff --git a/apps/perf/main.tsx b/apps/perf/main.tsx index 4db5f663..707f021c 100644 --- a/apps/perf/main.tsx +++ b/apps/perf/main.tsx @@ -1,20 +1,70 @@ -import { Cubit } from "@blac/core"; -import { useBloc } from "@blac/react"; -import React from "react"; -import { createRoot, Root } from "react-dom/client"; +import { Cubit } from '@blac/core'; +import { useBloc } from '@blac/react'; +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; import './bootstrap.css'; import './main.css'; interface DataItem { id: number; label: string; - selected: boolean; - removed: boolean; } -const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', 'cheap', 'expensive', 'fancy']; -const C = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange']; -const N = ['table', 'chair', 'house', 'bbq', 'desk', 'car', 'pony', 'cookie', 'sandwich', 'burger', 'pizza', 'mouse', 'keyboard']; +const A = [ + 'pretty', + 'large', + 'big', + 'small', + 'tall', + 'short', + 'long', + 'handsome', + 'plain', + 'quaint', + 'clean', + 'elegant', + 'easy', + 'angry', + 'crazy', + 'helpful', + 'mushy', + 'odd', + 'unsightly', + 'adorable', + 'important', + 'inexpensive', + 'cheap', + 'expensive', + 'fancy', +]; +const C = [ + 'red', + 'yellow', + 'blue', + 'green', + 'pink', + 'brown', + 'purple', + 'brown', + 'white', + 'black', + 'orange', +]; +const N = [ + 'table', + 'chair', + 'house', + 'bbq', + 'desk', + 'car', + 'pony', + 'cookie', + 'sandwich', + 'burger', + 'pizza', + 'mouse', + 'keyboard', +]; const random = (max: number): number => Math.round(Math.random() * 1000) % max; @@ -25,121 +75,128 @@ function buildData(count: number): DataItem[] { data[i] = { id: nextId++, label: `${A[random(A.length)]} ${C[random(C.length)]} ${N[random(N.length)]}`, - selected: false, - removed: false, }; } return data; } -// State management with Blac (Cubit) -class DemoBloc extends Cubit { +class DemoBloc extends Cubit<{ + selected: number[]; + data: DataItem[]; +}> { constructor() { - super([]); + super({ + selected: [], + data: [], + }); } run = (): void => { const data = buildData(1000); - this.emit(data); + this.emit({ + selected: [], + data, + }); }; runLots = (): void => { const data = buildData(10000); - this.emit(data); + this.emit({ + selected: [], + data, + }); }; add = (): void => { const addData = buildData(1000); - this.emit([...this.state, ...addData]); + const newState = [...this.state.data, ...addData]; + this.patch({ + data: newState, + }); }; update = (): void => { - let visibleItemCounter = 0; - const updatedData = this.state.map(item => { - if (item.removed) { - return item; - } - if (visibleItemCounter % 10 === 0) { - visibleItemCounter++; - return { ...item, label: item.label + " !!!" }; + const updatedData = this.state.data.map((item, i) => { + if (i % 10 === 0) { + return { ...item, label: item.label + ' !!!' }; } - visibleItemCounter++; return item; }); - this.emit(updatedData); + this.patch({ + data: updatedData, + }); }; - lastSelected: number = -1; - select = (index: number): void => { - const newData = [...this.state]; - - if (this.lastSelected !== -1 && this.lastSelected !== index) { - newData[this.lastSelected] = { ...newData[this.lastSelected], selected: false }; - } - - const item = newData[index]; - newData[index] = { ...item, selected: !item.selected }; - - this.lastSelected = index; - this.emit(newData); + select = (id: number): void => { + const currentSelected = this.state.selected; + const newSelected = currentSelected.includes(id) ? [] : [id]; + this.patch({ selected: newSelected }); }; - remove = (index: number): void => { - const newData = [...this.state]; - newData[index] = { ...newData[index], removed: true }; - this.emit(newData); + remove = (id: number): void => { + const newData = this.state.data.filter((item) => item.id !== id); + this.patch({ data: newData }); }; clear = (): void => { - this.emit([]); + this.emit({ + selected: [], + data: [], + }); }; swapRows = (): void => { - const currentData = this.state.filter(item => !item.removed); + const currentData = [...this.state.data]; const swappableData = [...currentData]; const tmp = swappableData[1]; swappableData[1] = swappableData[998]; swappableData[998] = tmp; - this.emit(swappableData); + this.patch({ data: swappableData }); }; } -const GlyphIcon =