{"_id":"@aaronbassett/block-party","name":"@aaronbassett/block-party","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aaronbassett/block-party","version":"0.1.0","description":"Lean block orchestration library for React - manages content blocks with single edit mode, limits, and drag & drop","main":"./lib/index.js","types":"./lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"build":"tsc --project tsconfig.build.json","clean":"rm -rf ./lib/","cm":"cz","lint":"eslint ./src/ --fix","prepare":"husky install","prepublishOnly":"npm run clean && npm run build","semantic-release":"semantic-release","test:watch":"jest --watch","test":"jest --coverage","typecheck":"tsc --noEmit","storybook":"storybook dev -p 6006","build-storybook":"storybook build -o docs-build"},"repository":{"type":"git","url":"git+https://github.com/aaronbassett/block-party.git"},"license":"MIT","author":{"name":"Aaron Bassett","email":"aaron@aaronbassett.com","url":"https://github.com/aaronbassett"},"engines":{"node":">=16.0"},"keywords":["react","blocks","content-blocks","block-editor","orchestration","drag-drop","dnd-kit","zustand","typescript"],"bugs":{"url":"https://github.com/aaronbassett/block-party/issues"},"homepage":"https://github.com/aaronbassett/block-party#readme","peerDependencies":{"react":"^18.0.0","react-dom":"^18.0.0"},"dependencies":{"@dnd-kit/core":"^6.3.0","@dnd-kit/sortable":"^8.0.0","@dnd-kit/utilities":"^3.2.2","@floating-ui/react":"^0.27.12","zod":"^3.24.1","zustand":"^5.0.2"},"devDependencies":{"@ryansonshine/commitizen":"^4.2.8","@ryansonshine/cz-conventional-changelog":"^3.3.4","@storybook/addon-docs":"^8.6.14","@storybook/addon-essentials":"^8.6.14","@storybook/addon-interactions":"^8.6.14","@storybook/addon-links":"^8.6.14","@storybook/blocks":"^8.6.14","@storybook/react":"^8.6.14","@storybook/react-vite":"^8.6.14","@storybook/test":"^8.6.14","@testing-library/jest-dom":"^5.16.5","@testing-library/react":"^13.4.0","@testing-library/user-event":"^14.6.1","@types/jest":"^29.5.14","@types/node":"^18.19.0","@types/react":"^18.3.0","@types/react-dom":"^18.3.0","@typescript-eslint/eslint-plugin":"^5.62.0","@typescript-eslint/parser":"^5.62.0","@vitejs/plugin-react":"^4.6.0","conventional-changelog-conventionalcommits":"^5.0.0","eslint":"^8.57.0","eslint-config-prettier":"^8.3.0","eslint-plugin-node":"^11.1.0","eslint-plugin-prettier":"^3.4.0","husky":"^6.0.0","jest":"^29.7.0","jest-environment-jsdom":"29.7.0","lint-staged":"^13.2.1","prettier":"^2.2.1","react":"^18.3.0","react-dom":"^18.3.0","semantic-release":"^21.0.1","storybook":"^8.6.14","ts-jest":"^29.2.5","ts-node":"^10.2.1","typescript":"^5.0.4","vite":"^6.3.5"},"config":{"commitizen":{"path":"./node_modules/@ryansonshine/cz-conventional-changelog"}},"lint-staged":{"*.ts":"eslint --cache --cache-location .eslintcache --fix"},"release":{"branches":["main"],"plugins":[["@semantic-release/commit-analyzer",{"preset":"conventionalcommits","releaseRules":[{"type":"build","scope":"deps","release":"patch"}]}],["@semantic-release/release-notes-generator",{"preset":"conventionalcommits","presetConfig":{"types":[{"type":"feat","section":"Features"},{"type":"fix","section":"Bug Fixes"},{"type":"build","section":"Dependencies and Other Build Updates","hidden":false}]}}],"@semantic-release/npm","@semantic-release/github"]},"_id":"@aaronbassett/block-party@0.1.0","gitHead":"5c12d98072267fd3c2c43ccf1a878529f07b3411","_nodeVersion":"23.10.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-kdgcvmtsDz2P1a1SjdmtrM0J5uiJn2cdjwJh02xXgaG+TZxkoL8UEXRwtB/9PUz4y7tvHHigrkfgFdV2uyZ1EA==","shasum":"cfbab44fa6358e0f5da5a13f4d77a9952179696f","tarball":"https://registry.npmjs.org/@aaronbassett/block-party/-/block-party-0.1.0.tgz","fileCount":15,"unpackedSize":47252,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDZBbHftfpQmDQxQisf287ievFKTd4MSD5KjXJAb63KYgIgdc+w0a6b9Cmj751qQA8V6QSnMMsYkmRIo4Vqd/ApGeM="}]},"_npmUser":{"name":"aaronbassett","email":"aaronbassett@gmail.com","actor":{"name":"aaronbassett","email":"aaronbassett@gmail.com","type":"user"}},"directories":{},"maintainers":[{"name":"aaronbassett","email":"aaronbassett@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/block-party_0.1.0_1751102754008_0.22975986411445626"},"_hasShrinkwrap":false}},"time":{"created":"2025-06-28T09:25:53.855Z","0.1.0":"2025-06-28T09:25:54.214Z","modified":"2025-06-28T09:25:54.515Z"},"maintainers":[{"name":"aaronbassett","email":"aaronbassett@gmail.com"}],"description":"Lean block orchestration library for React - manages content blocks with single edit mode, limits, and drag & drop","homepage":"https://github.com/aaronbassett/block-party#readme","keywords":["react","blocks","content-blocks","block-editor","orchestration","drag-drop","dnd-kit","zustand","typescript"],"repository":{"type":"git","url":"git+https://github.com/aaronbassett/block-party.git"},"author":{"name":"Aaron Bassett","email":"aaron@aaronbassett.com","url":"https://github.com/aaronbassett"},"bugs":{"url":"https://github.com/aaronbassett/block-party/issues"},"license":"MIT","readme":"# Block Party 🎉\n\n> Lean block orchestration library for React - manages content blocks with single edit mode, limits, and drag & drop\n\nBlock Party provides a flexible orchestration system for managing blocks of content in React applications. It handles the coordination of multiple block types, enforces single edit mode across all blocks, manages block limits per type, and provides a clean API for external components to interact with blocks.\n\n## Features\n\n- 🎯 **Single Edit Mode** - Only one block can be edited at a time across all managers\n- 🚫 **Block Limits** - Enforce maximum blocks per type\n- 🔄 **Drag & Drop** - Reorder blocks with dedicated drag handles that appear on hover\n- 📦 **State Tracking** - Track block states: empty, dirty, clean\n- ⌨️ **Keyboard Navigation** - Full keyboard support with Tab/Enter\n- 🎨 **Unstyled** - Bring your own styles, no CSS included\n- 🛡️ **Type Safe** - Full TypeScript support with generics\n- 🪶 **Lightweight** - ~400 lines of code, minimal dependencies\n- 🔌 **Flexible** - Bring your own block implementations\n\n## What This Library Does\n\n- Orchestrates multiple block managers on a page\n- Enforces single edit mode across all blocks\n- Manages block limits per type\n- Coordinates focus navigation between blocks\n- Provides a clean API for adding, removing, and updating blocks\n- Handles block reordering with drag & drop\n- Provides custom error classes for better debugging\n\n## What This Library Does NOT Do\n\n- Does NOT implement specific block types (that's your responsibility)\n- Does NOT handle persistence (blocks must bring their own save functions)\n- Does NOT manage block UI (blocks render themselves)\n- Does NOT validate block data (blocks handle their own validation)\n\n## Installation\n\n```bash\nnpm install @aaronbassett/block-party\n# or\nyarn add @aaronbassett/block-party\n# or\npnpm add @aaronbassett/block-party\n```\n\n## Quick Start\n\n```tsx\nimport { BlockManager, useBlockStore } from '@aaronbassett/block-party';\nimport type { BlockConfig, BlockEditProps, BlockRenderProps } from 'block-party';\n\n// Define your block data type\ninterface TextBlockData {\n  content: string;\n}\n\n// Create view component\nconst TextBlockView: React.FC<BlockRenderProps<TextBlockData>> = ({ block }) => (\n  <div>{block.data.content || 'Click to edit...'}</div>\n);\n\n// Create edit component\nconst TextBlockEdit: React.FC<BlockEditProps<TextBlockData>> = ({\n  block,\n  onChange,\n  onSave,\n  onCancel,\n}) => (\n  <div>\n    <input\n      value={block.data.content}\n      onChange={(e) => onChange({ content: e.target.value })}\n      onKeyDown={(e) => {\n        if (e.key === 'Enter') onSave();\n        if (e.key === 'Escape') onCancel();\n      }}\n    />\n    <button onClick={onSave}>Save</button>\n    <button onClick={onCancel}>Cancel</button>\n  </div>\n);\n\n// Create block configuration\nconst textBlockConfig: BlockConfig<TextBlockData> = {\n  type: 'text',\n  displayName: 'Text Block',\n  maxBlocks: 5,\n  renderView: (props) => <TextBlockView {...props} />,\n  renderEdit: (props) => <TextBlockEdit {...props} />,\n  createDefault: () => ({ content: '' }),\n  validate: (data) => data.content.length <= 1000,\n  onSave: async (block) => {\n    // Optional: persist to backend\n    console.log('Saving block:', block);\n  },\n};\n\n// Use in your app\nfunction App() {\n  return (\n    <BlockManager\n      type=\"text\"\n      config={textBlockConfig}\n      onError={(error) => console.error(error)}\n    />\n  );\n}\n```\n\n## API Reference\n\n### Types\n\n#### `Block<T>`\nCore block interface with state management:\n\n```typescript\ninterface Block<T = unknown> {\n  id: string;\n  type: string;\n  data: T;\n  order: number;\n  state: 'empty' | 'dirty' | 'clean';\n  isEditing: boolean;\n  createdAt: number;\n  updatedAt: number;\n  savedAt?: number;\n}\n```\n\n#### `BlockConfig<T>`\nConfiguration for each block type:\n\n```typescript\ninterface BlockConfig<T = unknown> {\n  type: string;\n  displayName: string;\n  maxBlocks?: number;\n  \n  // Render functions\n  renderView: (props: BlockRenderProps<T>) => ReactElement;\n  renderEdit: (props: BlockEditProps<T>) => ReactElement;\n  \n  // Lifecycle functions\n  createDefault: () => T;\n  validate?: (data: T) => boolean;\n  onSave?: (block: Block<T>) => Promise<void>;\n}\n```\n\n### Components\n\n#### `<BlockManager>`\nMain component for managing blocks of a specific type:\n\n```tsx\n<BlockManager\n  type=\"text\"\n  config={blockConfig}\n  className=\"my-blocks\"\n  onError={(error) => console.error(error)}\n/>\n```\n\n### Store\n\nAccess the global block store using the `useBlockStore` hook:\n\n```tsx\nconst store = useBlockStore();\n\n// Add a block\nconst blockId = store.addBlock('text');\n\n// Update block data\nstore.updateBlockData(blockId, { content: 'Hello' });\n\n// Enable edit mode (disables others)\nstore.enableBlockEdit(blockId);\n\n// Save block\nawait store.saveBlock(blockId);\n\n// Query blocks\nconst textBlocks = store.getBlocksByType('text');\nconst canAdd = store.canAddBlock('text');\n```\n\n## Advanced Example\n\nHere's a more complete example with multiple block types:\n\n```tsx\nimport { BlockManager, useBlockStore } from '@aaronbassett/block-party';\nimport type { BlockConfig } from 'block-party';\n\n// Text block implementation\nconst textBlockConfig: BlockConfig<{ content: string; format: string }> = {\n  type: 'text',\n  displayName: 'Text',\n  maxBlocks: 10,\n  renderView: ({ block }) => (\n    <div className={`text-${block.data.format}`}>\n      {block.data.content}\n    </div>\n  ),\n  renderEdit: ({ block, onChange, onSave, onCancel }) => (\n    <div>\n      <textarea\n        value={block.data.content}\n        onChange={(e) => onChange({ ...block.data, content: e.target.value })}\n      />\n      <select\n        value={block.data.format}\n        onChange={(e) => onChange({ ...block.data, format: e.target.value })}\n      >\n        <option value=\"normal\">Normal</option>\n        <option value=\"heading\">Heading</option>\n        <option value=\"quote\">Quote</option>\n      </select>\n      <button onClick={onSave}>Save</button>\n      <button onClick={onCancel}>Cancel</button>\n    </div>\n  ),\n  createDefault: () => ({ content: '', format: 'normal' }),\n  validate: (data) => data.content.length > 0 && data.content.length <= 5000,\n};\n\n// Image block implementation\nconst imageBlockConfig: BlockConfig<{ url: string; alt: string }> = {\n  type: 'image',\n  displayName: 'Image',\n  maxBlocks: 5,\n  renderView: ({ block }) => (\n    <img src={block.data.url} alt={block.data.alt} />\n  ),\n  renderEdit: ({ block, onChange, onSave, onCancel }) => (\n    <div>\n      <input\n        placeholder=\"Image URL\"\n        value={block.data.url}\n        onChange={(e) => onChange({ ...block.data, url: e.target.value })}\n      />\n      <input\n        placeholder=\"Alt text\"\n        value={block.data.alt}\n        onChange={(e) => onChange({ ...block.data, alt: e.target.value })}\n      />\n      <button onClick={onSave}>Save</button>\n      <button onClick={onCancel}>Cancel</button>\n    </div>\n  ),\n  createDefault: () => ({ url: '', alt: '' }),\n  validate: (data) => data.url.startsWith('http'),\n};\n\n// Page component with multiple block types\nfunction Page() {\n  const store = useBlockStore();\n  \n  return (\n    <div className=\"page\">\n      <h1>My Page</h1>\n      \n      <section>\n        <h2>Text Blocks</h2>\n        <BlockManager type=\"text\" config={textBlockConfig} />\n      </section>\n      \n      <section>\n        <h2>Image Blocks</h2>\n        <BlockManager type=\"image\" config={imageBlockConfig} />\n      </section>\n      \n      <button onClick={() => {\n        // Save all blocks\n        const allBlocks = [\n          ...store.getBlocksByType('text'),\n          ...store.getBlocksByType('image'),\n        ];\n        console.log('Saving all blocks:', allBlocks);\n      }}>\n        Save Page\n      </button>\n    </div>\n  );\n}\n```\n\n## Block States\n\nBlocks have three states that help track their lifecycle:\n\n- **`empty`** - New block with default data\n- **`dirty`** - Block has unsaved changes\n- **`clean`** - Block has been saved\n\nYou can check block state using the utility functions:\n\n```tsx\nimport { isBlockEmpty, isBlockDirty, isBlockClean } from 'block-party';\n\nconst block = store.getBlock(blockId);\nif (isBlockDirty(block)) {\n  console.log('Block has unsaved changes');\n}\n```\n\n## Keyboard Support\n\n- **Tab/Shift+Tab** - Navigate between blocks\n- **Enter** - Enter edit mode on focused block\n- **Escape** - Cancel editing (in your edit component)\n- **Ctrl/Cmd+S** - Save block (in your edit component)\n\n## Styling\n\nBlock Party is unstyled by default. Add your own CSS:\n\n```css\n/* Example styles */\n.block-container {\n  margin: 1rem 0;\n  padding: 1rem;\n  border: 1px solid #ccc;\n  cursor: pointer;\n}\n\n.block-container:focus {\n  outline: 2px solid blue;\n}\n\n.block-container[aria-grabbed=\"true\"] {\n  opacity: 0.5;\n}\n\n.add-block-button {\n  margin-top: 1rem;\n  padding: 0.5rem 1rem;\n  border: 2px dashed #ccc;\n  background: none;\n  cursor: pointer;\n}\n```\n\n## TypeScript\n\nBlock Party is written in TypeScript and provides full type safety:\n\n```tsx\nimport type { Block, BlockConfig, BlockEditProps, BlockRenderProps } from 'block-party';\n\n// Define your block data type\ninterface MyBlockData {\n  title: string;\n  content: string;\n}\n\n// Components get fully typed props\nconst MyBlockEdit: React.FC<BlockEditProps<MyBlockData>> = ({ block, onChange }) => {\n  // block.data is typed as MyBlockData\n  return <input value={block.data.title} onChange={(e) => onChange({ ...block.data, title: e.target.value })} />;\n};\n```\n\n## Publishing\n\nThis package is automatically published to npm when changes are merged to the `main` branch. The release process uses [semantic-release](https://github.com/semantic-release/semantic-release) to:\n\n1. Analyze commit messages to determine the next version\n2. Generate release notes from commit messages\n3. Publish to npm\n4. Create a GitHub release\n\n### Commit Message Format\n\nWe follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:\n\n- `feat:` - New features (triggers minor version bump)\n- `fix:` - Bug fixes (triggers patch version bump)\n- `docs:` - Documentation changes (no release)\n- `chore:` - Maintenance tasks (no release)\n- `refactor:` - Code refactoring (no release)\n- `test:` - Test changes (no release)\n\nBreaking changes should include `BREAKING CHANGE:` in the commit body or append `!` to the type (e.g., `feat!:`).\n\n### Setup for Publishing\n\n1. **Add NPM Token**: Go to your repository settings on GitHub and add a secret named `NPM_TOKEN` with your npm authentication token.\n\n2. **Ensure Main Branch Protection**: The release workflow runs on pushes to `main`, so protect your main branch and use pull requests.\n\n### Manual Publishing\n\nIf you need to publish manually:\n\n```bash\nnpm run clean\nnpm run build\nnpm publish\n```\n\n## License\n\nMIT © Aaron Bassett","readmeFilename":"README.md","_rev":"1-a9aed4f2d9bc87080ea92b555b2f7e72"}