{
  "schemaVersion": 1,
  "name": "resources",
  "title": "Resource library",
  "tier": "Free",
  "price": 0,
  "license": "MIT",
  "framework": "React 19",
  "dependencies": [
    "@bld8/ui"
  ],
  "description": "File selection, searchable metadata, CSV export and reversible archiving.",
  "notes": "Pass initialRecords, onImport and onRecordsChange for your integration. Without onImport, only metadata is added in memory; files are not hosted or uploaded. CSV exports chosen metadata with formula escaping. Archive has one-step undo. Server-side storage and validation are application concerns.",
  "files": [
    {
      "path": "packages/blocks/src/account-settings.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { Avatar, Button, Field, Input, Switch } from '@bld8/ui';\n\nexport interface AccountValues { name: string; email: string; digest: boolean }\nexport function AccountSettings({ initialValues = { name: 'Alex Morgan', email: 'alex@example.com', digest: true }, onSave }: { initialValues?: AccountValues; onSave?: (values: AccountValues) => Promise<void> }) {\n  const [values, setValues] = useState(initialValues);\n  const [errors, setErrors] = useState<{ name?: string; email?: string }>({});\n  const [saving, setSaving] = useState(false);\n  const [message, setMessage] = useState('');\n  function update(patch: Partial<AccountValues>) { setValues(current => ({ ...current, ...patch })); setMessage(''); }\n  async function save(event: FormEvent) {\n    event.preventDefault();\n    const next = { name: values.name.trim() ? undefined : 'Enter your name.', email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(values.email.trim()) ? undefined : 'Enter a valid email address.' };\n    setErrors(next); if (next.name || next.email) return;\n    setSaving(true);\n    try { if (onSave) await onSave({ ...values, name: values.name.trim(), email: values.email.trim() }); setMessage(onSave ? 'Changes saved.' : 'Saved for this session.'); }\n    catch { setMessage('Changes could not be saved. Please try again.'); }\n    finally { setSaving(false); }\n  }\n  return <section className=\"bblocks-panel\" aria-label=\"Account settings\">\n    <div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">PERSONAL SPACE</p><h2>Your profile.</h2><p className=\"bblocks-muted\">The details that make it yours.</p></div><Avatar name={values.name || 'Profile'} size=\"lg\" /></div>\n    <form onSubmit={save} noValidate className=\"bblocks-stack\">\n      <div className=\"bblocks-form-grid\"><Field label=\"Display name\" error={errors.name} required><Input value={values.name} autoComplete=\"name\" onChange={e => update({name:e.target.value})} /></Field><Field label=\"Email address\" error={errors.email} required><Input type=\"email\" value={values.email} autoComplete=\"email\" onChange={e => update({email:e.target.value})} /></Field></div>\n      <div className=\"bblocks-rule\"><Switch label=\"Weekly summary\" description=\"A quiet update about your workspace.\" checked={values.digest} onChange={e => update({digest:e.target.checked})} /></div>\n      <div className=\"bblocks-heading\"><p className=\"bblocks-note\">{onSave ? 'Save your profile changes.' : 'Sample profile. Changes stay in this session.'}</p><Button type=\"submit\" loading={saving}>Save profile</Button></div>\n    </form><p role=\"status\" className=\"bblocks-status\">{message}</p>\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/index.ts",
      "content": "'use client';\nexport * from './sign-in.js';\nexport * from './account-settings.js';\nexport * from './metrics.js';\nexport * from './workspace.js';\nexport * from './team.js';\nexport * from './project-board.js';\nexport * from './templates.js';\nexport * from './project-planner.js';\nexport * from './resource-manager.js';\nexport * from './operations-template.js';\nexport * from './outline-projects.js';\nexport * from './outline-repository.js';\n"
    },
    {
      "path": "packages/blocks/src/metrics.tsx",
      "content": "'use client';\nimport { useState } from 'react';\nimport { Badge, Button } from '@bld8/ui';\n\nconst periods = {\n  week: { label:'This week', visits:'8,420', projects:'24', completion:'86%', bars:[42,58,48,76,65,92,80], labels:['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] },\n  month: { label:'This month', visits:'32,680', projects:'91', completion:'89%', bars:[35,54,61,48,78,88,96], labels:['01','05','09','13','17','21','25'] },\n};\nexport function MetricsOverview() {\n  const [period, setPeriod] = useState<keyof typeof periods>('week');\n  const current = periods[period];\n  return <section className=\"bblocks-metrics\" aria-label=\"Metrics overview\">\n    <div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">THE BIG PICTURE</p><h2>A good rhythm.</h2></div><div className=\"bblocks-segment\" role=\"group\" aria-label=\"Metrics period\">{(Object.keys(periods) as (keyof typeof periods)[]).map(key => <Button key={key} size=\"sm\" variant={period === key ? 'primary' : 'ghost'} aria-pressed={period === key} onClick={() => setPeriod(key)}>{periods[key].label}</Button>)}</div></div>\n    <div className=\"bblocks-stats\">{[['Visits',current.visits,'+12%'],['Active projects',current.projects,'+4'],['Completion',current.completion,'+3%']].map(([label,value,change]) => <div className=\"bblocks-stat\" key={label}><span>{label}</span><strong>{value}</strong><Badge tone=\"success\">{change}</Badge></div>)}</div>\n    <figure className=\"bblocks-panel bblocks-chart\"><figcaption><span>Workspace activity</span><span className=\"bblocks-muted\">{current.label} · sample data</span></figcaption><div className=\"bblocks-bars\" role=\"img\" aria-label={`${current.label} sample activity: ${current.bars.map((value,i) => `${current.labels[i]} ${value}`).join(', ')}`}>{current.bars.map((height,index) => <div key={index}><span style={{height:height + '%'}} /><small>{current.labels[index]}</small></div>)}</div></figure>\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/operations-template.tsx",
      "content": "'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { CalendarDays, FolderOpen, Search, Settings2, Sparkle } from 'lucide-react';\nimport { Alert, Badge, Button, CommandPalette, ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, SidebarProvider, Sidebar, SidebarHeader, SidebarGroup, SidebarMenuButton, SidebarFooter, SidebarInset, SidebarTrigger } from '@bld8/ui';\nimport { OutlineProjects } from './outline-projects.js';\nimport { createOutlineRepository, type OutlineRepository } from './outline-repository.js';\nimport type { ResourceRecord } from './resource-manager.js';\nimport type { AccountValues } from './account-settings.js';\nimport { ResourceManager } from './resource-manager.js';\nimport { AccountSettings } from './account-settings.js';\n\nconst views=[{id:'Planning',icon:CalendarDays},{id:'Resources',icon:FolderOpen},{id:'Settings',icon:Settings2}] as const;\nexport function OperationsTemplate({repository:provided}:{repository?:OutlineRepository}={}) {\n  const [repository,setRepository]=useState<OutlineRepository|null>(provided ?? null);\n  const [storageError,setStorageError]=useState('');\n  const [preferences,setPreferences]=useState<{resources?:ResourceRecord[];profile?:AccountValues}|null>(null);\n  useEffect(()=>{try{setRepository(provided ?? createOutlineRepository(window.localStorage));const raw=window.localStorage.getItem('bld8-outline-preferences-v1');const value=raw?JSON.parse(raw):{};setPreferences({resources:Array.isArray(value.resources)?value.resources:undefined,profile:value.profile && typeof value.profile.name==='string' && typeof value.profile.email==='string' && typeof value.profile.digest==='boolean'?value.profile:undefined});}catch{setStorageError('Browser storage is unavailable. Enable storage for this site and reload to use the saved workspace.');setPreferences({});}},[provided]);\n  const preferenceRef=useRef(preferences);preferenceRef.current=preferences;\n  function savePreferences(patch:Partial<NonNullable<typeof preferences>>){const next={...preferenceRef.current,...patch};window.localStorage.setItem('bld8-outline-preferences-v1',JSON.stringify(next));preferenceRef.current=next;setPreferences(next);}\n\n  const [view,setView]=useState('Planning');const heading=useRef<HTMLHeadingElement>(null);\n  function navigate(next:string) {setView(next);requestAnimationFrame(()=>heading.current?.focus());}\n  return <div className=\"bblocks-operations\">\n    <header className=\"bblocks-operations-header\"><span className=\"bblocks-wordmark\"><span aria-hidden=\"true\">✳</span> Outline</span><Badge tone=\"accent\">Example application</Badge><CommandPalette title=\"Outline navigation\" trigger={<Button variant=\"secondary\" size=\"sm\" leadingIcon={Search}>Find a view</Button>} items={views.map(item=>({id:item.id,label:'Open '+item.id.toLowerCase(),category:'Navigate',icon:item.icon,onSelect:()=>navigate(item.id)}))} /></header>\n    <SidebarProvider className=\"bblocks-operations-layout\">\n      <Sidebar label=\"Outline views\">\n        <SidebarHeader><Sparkle size={18} strokeWidth={1.75} aria-hidden=\"true\" /><span className=\"bui-sidebar-label\">Your workspace</span></SidebarHeader>\n        <SidebarGroup label=\"WORKSPACE\">{views.map(item=><SidebarMenuButton key={item.id} icon={item.icon} label={item.id} active={view===item.id} onClick={()=>setView(item.id)} />)}</SidebarGroup>\n        <SidebarFooter>Small steps. Room to think.</SidebarFooter>\n      </Sidebar>\n      <SidebarInset>\n        <div className=\"bblocks-operations-toolbar\"><SidebarTrigger /><span className=\"bblocks-muted\">{view}</span></div>\n        <ContextMenu><ContextMenuTrigger className=\"bblocks-operations-intro\" aria-label=\"Workspace quick navigation\">\n          <p className=\"bblocks-kicker\">SMALL PLANS. USEFUL DETAILS.</p><h2 ref={heading} tabIndex={-1}>{view==='Planning' ? 'Make room for the next idea.' : view==='Resources' ? 'Keep the references close.' : 'A few details, made yours.'}</h2><p className=\"bblocks-muted\">Projects, references and preferences. Saved in this browser; no account required.</p>\n        </ContextMenuTrigger><ContextMenuContent aria-label=\"Workspace quick navigation\">{views.map(item=><ContextMenuItem key={item.id} onClick={()=>navigate(item.id)}><item.icon size={16} aria-hidden=\"true\" />Open {item.id.toLowerCase()}</ContextMenuItem>)}</ContextMenuContent></ContextMenu>\n        {storageError && <Alert title=\"Storage needs attention\" tone=\"warning\">{storageError}</Alert>}\n        <div hidden={view!=='Planning'}>{repository?<OutlineProjects repository={repository} />:<p role=\"status\">Opening the workspace…</p>}</div>\n        <div hidden={view!=='Resources'}>{preferences && <ResourceManager initialRecords={preferences.resources} onRecordsChange={resources=>{try{savePreferences({resources});setStorageError('');}catch{setStorageError('Resource changes are only in memory: browser storage could not be updated.');}}} />}</div>\n        <div hidden={view!=='Settings'}>{preferences && <AccountSettings initialValues={preferences.profile} onSave={async profile=>{savePreferences({profile});setStorageError('');}} />}</div>\n      </SidebarInset>\n    </SidebarProvider>\n    <footer className=\"bblocks-note\">Outline / BLD8 UI · MIT · Both collections free. Local browser storage · File metadata only · Connect the repository adapter to your own API.</footer>\n  </div>;\n}\n"
    },
    {
      "path": "packages/blocks/src/outline-projects.tsx",
      "content": "'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { Archive, Check, Download, Pencil, Plus, Trash2 } from 'lucide-react';\nimport { Alert, Badge, Button, DataTable, Dialog, Field, Form, FormErrorSummary, Input, Textarea, createTableCSV, defaultTableQuery, useForm, useUnsavedChanges, type Column, type TableQuery } from '@bld8/ui';\nimport { OutlineValidationError, validateOutline, type OutlineProject, type OutlineRepository, type OutlineValues } from './outline-repository.js';\n\nconst empty:OutlineValues={name:'',due:'',notes:'',status:'Planned'};\nfunction ProjectEditor({project,repository,onSaved,onClose}:{project:OutlineProject|null;repository:OutlineRepository;onSaved:()=>void;onClose:()=>void}) {\n  const [discard,setDiscard]=useState(false);\n  const form=useForm<OutlineValues>({\n    initialValues:project?{name:project.name,due:project.due,notes:project.notes,status:project.status}:empty,\n    validate:validateOutline,\n    onSubmit:async(values,{signal})=>{try{const saved=await repository.save(values,project,signal);return {values:saved};}catch(error){if(error instanceof OutlineValidationError)return {fieldErrors:error.fields};throw error;}},\n    onSuccess:onSaved,\n  });\n  useUnsavedChanges(form.dirty);\n  const close=()=>{if(form.pending)return;if(form.dirty)setDiscard(true);else onClose();};\n  return <Dialog title={project?'Edit project':'Plan a project'} description=\"A name, a review date and the context worth keeping.\" open onOpenChange={open=>{if(!open)close();}}>\n    <Form form={form} className=\"bblocks-stack\">\n      <FormErrorSummary error={form.error} />\n      <Field label=\"Project name\" required error={form.errors.name}><Input name=\"name\" value={form.values.name} onChange={event=>form.setField('name',event.target.value)} disabled={form.pending} autoComplete=\"off\" maxLength={80} /></Field>\n      <div className=\"bblocks-form-grid\"><Field label=\"Review date\" required error={form.errors.due}><Input name=\"due\" type=\"date\" value={form.values.due} onChange={event=>form.setField('due',event.target.value)} disabled={form.pending} /></Field><label className=\"bui-field\"><span className=\"bui-label\">Status</span><select name=\"status\" className=\"bui-input\" value={form.values.status} onChange={event=>form.setField('status',event.target.value as OutlineValues['status'])} disabled={form.pending}>{['Planned','Ready','Archived'].map(value=><option key={value}>{value}</option>)}</select></label></div>\n      <Field label=\"Notes to self\" hint=\"Keep the useful context close.\" error={form.errors.notes}><Textarea name=\"notes\" value={form.values.notes} maxLength={1000} onChange={event=>form.setField('notes',event.target.value)} disabled={form.pending} /></Field>\n      {discard && <Alert title=\"Discard this draft?\" tone=\"warning\"><p>Your unsaved changes will be lost.</p><div className=\"bblocks-inline-actions\"><Button variant=\"danger\" size=\"sm\" onClick={onClose}>Discard changes</Button><Button variant=\"secondary\" size=\"sm\" onClick={()=>setDiscard(false)}>Keep editing</Button></div></Alert>}\n      <div className=\"bblocks-heading\"><span className=\"bblocks-note\">{form.pending?'Saving…':form.dirty?'Unsaved changes':'Ready when you are.'}</span><div className=\"bblocks-inline-actions\"><Button variant=\"secondary\" onClick={close} disabled={form.pending}>Cancel</Button><Button type=\"submit\" loading={form.pending}>{project?'Save project':'Add project'}</Button></div></div>\n    </Form>\n  </Dialog>;\n}\nexport function OutlineProjects({repository}:{repository:OutlineRepository}) {\n  const [query,setQuery]=useState<TableQuery>({...defaultTableQuery});const [rows,setRows]=useState<OutlineProject[]>([]);const [total,setTotal]=useState(0);\n  const [loading,setLoading]=useState(true);const [error,setError]=useState('');const [revision,setRevision]=useState(0);\n  const [selected,setSelected]=useState<string[]>([]);const [editor,setEditor]=useState<{project:OutlineProject|null}|null>(null);\n  const [deleting,setDeleting]=useState<OutlineProject|null>(null);const [mutation,setMutation]=useState(false);const [message,setMessage]=useState('');const [actionError,setActionError]=useState('');\n  const [density,setDensity]=useState<'comfortable'|'compact'>('comfortable');\n  const origin=useRef<HTMLElement|null>(null);const operation=useRef<AbortController|null>(null);\n  useEffect(()=>()=>operation.current?.abort(),[]);\n  useEffect(()=>{const controller=new AbortController();setLoading(true);setError('');repository.list(query,controller.signal).then(result=>{if(controller.signal.aborted)return;const last=Math.max(1,Math.ceil(result.total/query.pageSize));if(query.page>last){setQuery(current=>({...current,page:last}));return;}setRows(result.rows);setTotal(result.total);}).catch(error=>{if(!controller.signal.aborted)setError(error instanceof Error?error.message:'Unable to load projects.');}).finally(()=>{if(!controller.signal.aborted)setLoading(false);});return()=>controller.abort();},[query,revision,repository]);\n  useEffect(()=>{const refresh=()=>setRevision(value=>value+1);window.addEventListener('storage',refresh);return()=>window.removeEventListener('storage',refresh);},[]);\n  function openEditor(project:OutlineProject|null){origin.current=document.activeElement as HTMLElement;setEditor({project});}\n  function closeEditor(){setEditor(null);requestAnimationFrame(()=>origin.current?.focus());}\n  async function mutate(action:(signal:AbortSignal)=>Promise<void>,success:string){if(operation.current)return;const controller=new AbortController();operation.current=controller;setMutation(true);setActionError('');try{await action(controller.signal);if(controller.signal.aborted)return;setSelected([]);setDeleting(null);setMessage(success);setRevision(value=>value+1);}catch(error){if(!controller.signal.aborted)setActionError(error instanceof Error?error.message:'Changes could not be saved.');}finally{if(operation.current===controller){operation.current=null;setMutation(false);}}}\n  const columns:Column<OutlineProject>[]=[\n    {key:'name',header:'Project',hideable:false,pinned:'start',width:240,minWidth:160,sortValue:row=>row.name,cell:row=><Button variant=\"ghost\" size=\"sm\" className=\"bblocks-project-name\" onClick={()=>openEditor(row)}>{row.name}</Button>},\n    {key:'due',header:'Review date',width:160,sortValue:row=>row.due,cell:row=><time dateTime={row.due}>{row.due}</time>},\n    {key:'status',header:'Status',width:140,sortValue:row=>row.status,cell:row=><Badge tone={row.status==='Ready'?'success':'neutral'}>{row.status}</Badge>},\n    {key:'actions',header:'Actions',width:160,cell:row=><div className=\"bblocks-inline-actions\"><Button size=\"sm\" variant=\"ghost\" aria-label={'Edit '+row.name} leadingIcon={Pencil} onClick={()=>openEditor(row)}>Edit</Button><Button size=\"sm\" variant=\"ghost\" aria-label={'Delete '+row.name} onClick={()=>{setActionError('');setDeleting(row);}}><Trash2 size={16} aria-hidden=\"true\" /></Button></div>},\n  ];\n  function download(){const url=URL.createObjectURL(new Blob(['\\uFEFF'+createTableCSV(rows,columns)],{type:'text/csv;charset=utf-8;'}));const link=document.createElement('a');link.href=url;link.download='outline-current-page.csv';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);setMessage('Current page exported.');}\n  return <section className=\"bblocks-panel\" aria-label=\"Outline projects\"><div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">PROJECTS / {total.toString().padStart(2,'0')}</p><h2>A little room to plan.</h2><p className=\"bblocks-muted\">The next step, and the details behind it.</p></div><Button leadingIcon={Plus} onClick={()=>openEditor(null)}>Plan project</Button></div>\n    <div className=\"bblocks-heading bblocks-table-options\"><label className=\"bui-field\"><span className=\"bui-sr-only\">Table density</span><select aria-label=\"Table density\" className=\"bui-input\" value={density} onChange={event=>setDensity(event.target.value as typeof density)}><option value=\"comfortable\">Comfortable</option><option value=\"compact\">Compact</option></select></label><Button leadingIcon={Download} size=\"sm\" variant=\"ghost\" onClick={download} disabled={loading || !!error || !rows.length}>Export current page</Button></div>\n    {actionError && !deleting && <Alert title=\"Unable to save changes\" tone=\"danger\" live>{actionError}</Alert>}\n    <DataTable caption=\"Outline projects\" rows={rows} columns={columns} rowKey={row=>row.id} rowLabel={row=>row.name} server={{query,onQueryChange:setQuery,totalRows:total}} loading={loading} error={error} onRetry={()=>setRevision(value=>value+1)} columnVisibility resizableColumns density={density} selectable selectedKeys={selected} onSelectedKeysChange={setSelected} filters={[{key:'status',label:'Statuses',options:['Planned','Ready','Archived'].map(value=>({value,label:value})),value:row=>row.status}]} renderSelectedActions={keys=><><Button size=\"sm\" leadingIcon={Check} loading={mutation} onClick={()=>mutate(signal=>repository.setStatus(keys,'Ready',signal),keys.length+' projects marked ready.')}>Mark ready</Button><Button size=\"sm\" variant=\"secondary\" leadingIcon={Archive} disabled={mutation} onClick={()=>mutate(signal=>repository.setStatus(keys,'Archived',signal),keys.length+' projects archived.')}>Archive</Button></>} />\n    <p role=\"status\" className=\"bblocks-status\">{message}</p>\n    {editor && <ProjectEditor project={editor.project} repository={repository} onClose={closeEditor} onSaved={()=>{setMessage(editor.project?'Project saved.':'Project added.');setQuery(current=>({...current,page:1}));setRevision(value=>value+1);closeEditor();}} />}\n    {deleting && <Dialog title=\"Delete project?\" description={'Delete '+deleting.name+' permanently from this workspace.'} open onOpenChange={open=>{if(!open&&!mutation)setDeleting(null);}}>{actionError && <Alert title=\"Unable to delete project\" tone=\"danger\" live>{actionError}</Alert>}<div className=\"bblocks-inline-actions\"><Button variant=\"secondary\" disabled={mutation} onClick={()=>setDeleting(null)}>Keep project</Button><Button variant=\"danger\" loading={mutation} onClick={()=>mutate(signal=>repository.remove(deleting,signal),'Project deleted.')}>Delete project</Button></div></Dialog>}\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/outline-repository.ts",
      "content": "import type { FormErrors, TableQuery } from '@bld8/ui';\n\nexport interface OutlineValues {name:string; due:string; notes:string; status:'Planned'|'Ready'|'Archived'}\nexport interface OutlineProject extends OutlineValues {id:string;version:number}\nexport interface OutlinePage {rows:OutlineProject[];total:number}\nexport interface OutlineRepository {\n  list:(query:TableQuery,signal:AbortSignal)=>Promise<OutlinePage>;\n  save:(values:OutlineValues,previous:OutlineProject|null,signal:AbortSignal)=>Promise<OutlineProject>;\n  setStatus:(ids:string[],status:OutlineValues['status'],signal:AbortSignal)=>Promise<void>;\n  remove:(project:OutlineProject,signal:AbortSignal)=>Promise<void>;\n}\nexport class OutlineValidationError extends Error {\n  constructor(public fields:FormErrors<OutlineValues>){super('Check the highlighted fields.');}\n}\nexport const outlineSeed:OutlineProject[]=[\n  ['keyboard','Keyboard review','2026-10-20','Planned'],['interface','Interface studies','2026-10-24','Planned'],\n  ['forms','Form patterns','2026-10-28','Ready'],['navigation','Navigation audit','2026-11-02','Planned'],\n  ['tokens','Theme tokens','2026-11-06','Ready'],['docs','Usage guide','2026-11-10','Planned'],\n  ['motion','Motion studies','2026-11-14','Planned'],['mobile','Mobile checks','2026-11-18','Ready'],\n].map(([id,name,due,status])=>({id,name,due,status:status as OutlineValues['status'],notes:'',version:1}));\nexport function validateOutline(values:OutlineValues):FormErrors<OutlineValues> {\n  const fields:FormErrors<OutlineValues>={};\n  if(values.name.trim().length<3)fields.name='Use at least three characters.';\n  if(values.name.trim().length>80)fields.name='Keep the name within 80 characters.';\n  const parsed=Date.parse(values.due+'T12:00:00Z');\n  if(!/^\\d{4}-\\d{2}-\\d{2}$/.test(values.due) || Number.isNaN(parsed) || new Date(parsed).toISOString().slice(0,10)!==values.due)fields.due='Choose a valid review date.';\n  if(values.notes.length>1000)fields.notes='Keep notes within 1,000 characters.';\n  if(!['Planned','Ready','Archived'].includes(values.status))fields.status='Choose a valid status.';\n  return fields;\n}\nfunction readProjects(storage:Storage,key:string):OutlineProject[] {\n  const raw=storage.getItem(key);if(!raw)return outlineSeed.map(row=>({...row}));\n  const value:unknown=JSON.parse(raw);\n  if(!Array.isArray(value) || !value.every(row=>row && typeof row.id==='string' && typeof row.name==='string' && typeof row.notes==='string' && typeof row.due==='string' && ['Planned','Ready','Archived'].includes(row.status) && Number.isInteger(row.version)))throw new Error('Saved project data could not be read. Export or clear this site’s storage before starting again.');\n  return value;\n}\n/** Browser-backed reference adapter. Replace this contract with your HTTP service. */\nexport function createOutlineRepository(storage:Storage,key='bld8-outline-projects-v1'):OutlineRepository {\n  const read=()=>readProjects(storage,key);\n  const write=(rows:OutlineProject[])=>{try{storage.setItem(key,JSON.stringify(rows));}catch{throw new Error('Browser storage is unavailable or full. Your changes have not been saved. Try again.');}};\n  const check=(signal:AbortSignal)=>signal.throwIfAborted();\n  return {\n    async list(query,signal){\n      check(signal);let rows=read().filter(row=>(row.name+' '+row.notes).toLocaleLowerCase().includes(query.search.toLocaleLowerCase()) && (!query.filters.status || row.status===query.filters.status));\n      const sort=query.sort;if(sort && ['name','due','status'].includes(sort.key)){const key=sort.key as 'name'|'due'|'status';rows.sort((a,b)=>a[key].localeCompare(b[key],undefined,{numeric:true})*(sort.descending?-1:1));}\n      const total=rows.length;rows=rows.slice((query.page-1)*query.pageSize,query.page*query.pageSize);check(signal);return {rows,total};\n    },\n    async save(values,previous,signal){\n      check(signal);const fields=validateOutline(values);const rows=read();\n      if(rows.some(row=>row.id!==previous?.id && row.name.toLocaleLowerCase()===values.name.trim().toLocaleLowerCase()))fields.name='A project with this name already exists.';\n      if(Object.values(fields).some(Boolean))throw new OutlineValidationError(fields);\n      const existing=previous?rows.find(row=>row.id===previous.id):null;\n      if(previous && existing?.version!==previous.version)throw new Error('This project changed in another tab. Close and reopen it before saving. Your draft is still here.');\n      const project={...values,name:values.name.trim(),notes:values.notes.trim(),id:previous?.id ?? crypto.randomUUID(),version:(previous?.version ?? 0)+1};\n      check(signal);write(previous?rows.map(row=>row.id===previous.id?project:row):[project,...rows]);return project;\n    },\n    async setStatus(ids,status,signal){check(signal);write(read().map(row=>ids.includes(row.id)?{...row,status,version:row.version+1}:row));},\n    async remove(project,signal){check(signal);const rows=read();if(rows.find(row=>row.id===project.id)?.version!==project.version)throw new Error('This project changed. Refresh the list before deleting it.');write(rows.filter(row=>row.id!==project.id));},\n  };\n}\n"
    },
    {
      "path": "packages/blocks/src/project-board.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { Badge, Button, Dialog, Field, Input, Select } from '@bld8/ui';\n\nconst stages = ['Planned','In progress','Done'] as const;\ntype Stage = typeof stages[number];\nexport interface BoardTask { id:string; title:string; category:string; stage:Stage }\nconst sample: BoardTask[] = [\n  {id:'1',title:'Review keyboard navigation',category:'Accessibility',stage:'Planned'},\n  {id:'2',title:'Refine the empty state',category:'Interface',stage:'Planned'},\n  {id:'3',title:'Build the settings form',category:'Engineering',stage:'In progress'},\n  {id:'4',title:'Set the spacing scale',category:'Foundations',stage:'Done'},\n];\nexport function ProjectBoard({ initialTasks = sample }: { initialTasks?: BoardTask[] }) {\n  const [tasks,setTasks] = useState(initialTasks);\n  const [open,setOpen] = useState(false);\n  const [title,setTitle] = useState('');\n  const [error,setError] = useState('');\n  const [message,setMessage] = useState('');\n  function add(event: FormEvent) {\n    event.preventDefault(); if (!title.trim()) {setError('Enter a task title.');return;}\n    setTasks(current => [...current,{id:crypto.randomUUID(),title:title.trim(),category:'Workspace',stage:'Planned'}]);\n    setOpen(false);setTitle('');setError('');setMessage('Task added for this session.');\n  }\n  function move(id:string,stage:Stage) { setTasks(current => current.map(task => task.id === id ? {...task,stage} : task)); setMessage(`Task moved to ${stage.toLowerCase()}.`); }\n  return <section className=\"bblocks-board\" aria-label=\"Project board\"><div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">A LITTLE MOMENTUM</p><h2>From idea to done.</h2><p className=\"bblocks-muted\">Sample tasks. Change a status or add your own.</p></div><Dialog title=\"Add a task\" description=\"Create a task for this session.\" open={open} onOpenChange={setOpen} trigger={<Button>New task</Button>}><form onSubmit={add} noValidate className=\"bblocks-stack\"><Field label=\"Task title\" error={error} required><Input value={title} onChange={e => setTitle(e.target.value)} /></Field><Button type=\"submit\">Add task</Button></form></Dialog></div><div className=\"bblocks-columns\">{stages.map(stage => <section className=\"bblocks-lane\" key={stage} aria-label={stage + ' tasks'}><div className=\"bblocks-heading\"><h3>{stage}</h3><Badge>{tasks.filter(task => task.stage === stage).length}</Badge></div>{tasks.filter(task => task.stage === stage).map(task => <article className=\"bblocks-task\" key={task.id}><span className=\"bblocks-kicker\">{task.category}</span><h4>{task.title}</h4><Select label={'Status for ' + task.title} value={task.stage} options={stages.map(value => ({value,label:value}))} onValueChange={value => move(task.id,value as Stage)} /></article>)}{!tasks.some(task => task.stage === stage) && <p className=\"bblocks-note\">A little room for the next task.</p>}</section>)}</div><p className=\"bblocks-status\" role=\"status\">{message}</p></section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/project-planner.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { CalendarDays, Check, Plus } from 'lucide-react';\nimport { Alert, Badge, Button, DataTable, DatePicker, Dialog, Field, FileUpload, Icon, Input, MultiSelect, formatDateValue, type Column } from '@bld8/ui';\n\nexport interface PlannedProject { id:string; name:string; due:string; disciplines:string[]; status:'Planned'|'Ready'; attachmentCount:number }\nexport interface ProjectDraft { name:string; due:Date; disciplines:string[]; attachments:File[] }\nexport interface ProjectPlannerProps { initialProjects?:PlannedProject[]; onCreate?:(draft:ProjectDraft)=>Promise<PlannedProject>; onProjectsChange?:(projects:PlannedProject[])=>void }\nconst disciplines=['Design','Engineering','Research','Documentation'].map(label=>({value:label.toLowerCase(),label}));\nconst initial:PlannedProject[]=[\n  {id:'1',name:'Keyboard review',due:'2026-10-20',disciplines:['engineering'],status:'Planned',attachmentCount:2},\n  {id:'2',name:'Interface studies',due:'2026-10-24',disciplines:['design','research'],status:'Planned',attachmentCount:1},\n  {id:'3',name:'Form patterns',due:'2026-10-28',disciplines:['engineering','documentation'],status:'Ready',attachmentCount:0},\n];\nconst columns:Column<PlannedProject>[]=[\n  {key:'name',header:'Project',hideable:false,sortValue:row=>row.name,cell:row=><strong>{row.name}</strong>},\n  {key:'due',header:'Review date',sortValue:row=>row.due,cell:row=><span className=\"bblocks-date\"><Icon icon={CalendarDays} size={14} /><time dateTime={row.due}>{row.due.split('-').reverse().join('/')}</time></span>},\n  {key:'disciplines',header:'Disciplines',cell:row=><span className=\"bblocks-tags\">{row.disciplines.map(value=><Badge key={value}>{disciplines.find(option=>option.value===value)?.label ?? value}</Badge>)}</span>},\n  {key:'status',header:'Status',sortValue:row=>row.status,cell:row=><Badge tone={row.status==='Ready' ? 'success' : 'neutral'}>{row.status}</Badge>},\n];\nexport function ProjectPlanner({initialProjects=initial,onCreate,onProjectsChange}:ProjectPlannerProps) {\n  const [projects,setProjects]=useState(initialProjects); const [selected,setSelected]=useState<string[]>([]);\n  const [open,setOpen]=useState(false); const [name,setName]=useState(''); const [due,setDue]=useState<Date|null>(null);\n  const [tags,setTags]=useState<string[]>(['design']); const [files,setFiles]=useState<File[]>([]);\n  const [error,setError]=useState(''); const [busy,setBusy]=useState(false); const [message,setMessage]=useState('');\n  const change=(next:PlannedProject[])=>{setProjects(next);onProjectsChange?.(next);};\n  async function create(event:FormEvent) {\n    event.preventDefault(); if(!name.trim()) {setError('Enter a project name.');return;} if(!due) {setError('Choose a review date.');return;}\n    if(!tags.length) {setError('Choose at least one discipline.');return;} setError('');setBusy(true);\n    try {\n      const project=onCreate ? await onCreate({name:name.trim(),due,disciplines:tags,attachments:files}) : {id:crypto.randomUUID(),name:name.trim(),due:formatDateValue(due),disciplines:tags,status:'Planned' as const,attachmentCount:files.length};\n      change([...projects,project]);setName('');setDue(null);setTags(['design']);setFiles([]);setOpen(false);setMessage(onCreate ? 'Project added.' : 'Project metadata added for this session. Attachments were not uploaded.');\n    } catch(cause) {setError(cause instanceof Error ? cause.message : 'Unable to create the project. Try again.');} finally {setBusy(false);}\n  }\n  return <section className=\"bblocks-panel\" aria-label=\"Project planner\">\n    <div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">THE NEXT USEFUL STEP</p><h2>A little room to plan.</h2><p className=\"bblocks-muted\">Projects, disciplines and review dates in one place.</p></div><Dialog title=\"Plan a project\" description=\"Give the work a name and a review date.\" open={open} onOpenChange={next=>{if(!busy)setOpen(next);}} trigger={<Button leadingIcon={Plus}>Plan project</Button>}>\n      <form className=\"bblocks-stack\" noValidate onSubmit={create}>\n        <Field label=\"Project name\" required error={error && !name.trim() ? error : undefined}><Input value={name} onChange={event=>{setName(event.target.value);setError('');}} disabled={busy} autoComplete=\"off\" /></Field>\n        <DatePicker label=\"Review date\" value={due} onValueChange={setDue} disabled={busy} error={error && name.trim() && !due ? error : undefined} />\n        <MultiSelect label=\"Disciplines\" options={disciplines} value={tags} onValueChange={setTags} disabled={busy} error={error && name.trim() && due && !tags.length ? error : undefined} />\n        {error && name.trim() && due && tags.length>0 && <Alert title=\"Unable to add project\" tone=\"danger\" live>{error}</Alert>}\n        <FileUpload label=\"Reference files\" value={files} onValueChange={setFiles} accept=\".pdf,.txt,.png,.jpg\" maxFiles={3} maxSize={5*1024*1024} disabled={busy} />\n        <p className=\"bblocks-note\">{onCreate ? 'Your create callback handles persistence and attached files.' : 'This preview stores metadata in memory. Files are not uploaded or saved.'}</p>\n        <Button type=\"submit\" loading={busy}>Add project</Button>\n      </form>\n    </Dialog></div>\n    <div className=\"bblocks-planner-summary\"><span><strong>{projects.length}</strong> projects</span><span><strong>{projects.filter(project=>project.status==='Ready').length}</strong> ready for review</span><span><strong>{new Set(projects.flatMap(project=>project.disciplines)).size}</strong> disciplines</span></div>\n    <DataTable caption=\"Planned projects\" rows={projects} columns={columns} rowKey={row=>row.id} rowLabel={row=>row.name} searchText={row=>row.name+' '+row.disciplines.join(' ')} columnVisibility selectable selectedKeys={selected} onSelectedKeysChange={setSelected} filters={[{key:'status',label:'Statuses',options:[{value:'Planned',label:'Planned'},{value:'Ready',label:'Ready'}],value:row=>row.status}]} renderBulkActions={chosen=><Button size=\"sm\" leadingIcon={Check} onClick={()=>{change(projects.map(project=>chosen.some(row=>row.id===project.id) ? {...project,status:'Ready'} : project));setSelected([]);setMessage(chosen.length+' projects marked ready.');}}>Mark ready</Button>} />\n    <p role=\"status\" className=\"bblocks-status\">{message}</p>\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/resource-manager.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { Archive, Download, FileText, Plus, Undo2 } from 'lucide-react';\nimport { Alert, Badge, Button, DataTable, Dialog, FileUpload, Icon, createTableCSV, type Column } from '@bld8/ui';\n\nexport interface ResourceRecord {id:string;name:string;type:string;size:number;status:'Active'|'Archived'}\nexport interface ResourceManagerProps { initialRecords?:ResourceRecord[]; onImport?:(files:File[])=>Promise<ResourceRecord[]>; onRecordsChange?:(records:ResourceRecord[])=>void }\nconst initial:ResourceRecord[]=[\n  {id:'1',name:'Interface brief.pdf',type:'PDF',size:148200,status:'Active'},\n  {id:'2',name:'Research notes.txt',type:'TXT',size:4200,status:'Active'},\n  {id:'3',name:'Navigation sketches.png',type:'PNG',size:824000,status:'Active'},\n  {id:'4',name:'Old checklist.txt',type:'TXT',size:2080,status:'Archived'},\n];\nconst columns:Column<ResourceRecord>[]=[\n  {key:'name',header:'File',hideable:false,sortValue:row=>row.name,cell:row=><span className=\"bblocks-date\"><Icon icon={FileText} /><strong>{row.name}</strong></span>},\n  {key:'type',header:'Type',sortValue:row=>row.type,cell:row=><Badge>{row.type}</Badge>},\n  {key:'size',header:'Size',sortValue:row=>row.size,cell:row=><span>{(row.size/1024).toFixed(1)} KB</span>},\n  {key:'status',header:'Status',sortValue:row=>row.status,cell:row=><Badge tone={row.status==='Active' ? 'success' : 'neutral'}>{row.status}</Badge>},\n];\nfunction download(rows:ResourceRecord[]) {\n  const blob=new Blob(['\\uFEFF'+createTableCSV(rows,columns)],{type:'text/csv;charset=utf-8;'}); const url=URL.createObjectURL(blob);\n  const link=document.createElement('a');link.href=url;link.download='resource-metadata.csv';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);\n}\nexport function ResourceManager({initialRecords=initial,onImport,onRecordsChange}:ResourceManagerProps) {\n  const [records,setRecords]=useState(initialRecords);const [selected,setSelected]=useState<string[]>([]);const [files,setFiles]=useState<File[]>([]);\n  const [open,setOpen]=useState(false);const [busy,setBusy]=useState(false);const [error,setError]=useState('');const [message,setMessage]=useState('');\n  const [undo,setUndo]=useState<{id:string;status:ResourceRecord['status']}[]>([]);\n  const change=(next:ResourceRecord[])=>{setRecords(next);onRecordsChange?.(next);};\n  async function add(event:FormEvent) {\n    event.preventDefault();if(!files.length) {setError('Choose at least one file.');return;} setError('');setBusy(true);\n    try {\n      const added=onImport ? await onImport(files) : files.map(file=>({id:crypto.randomUUID(),name:file.name,type:file.name.split('.').pop()?.toUpperCase() ?? 'FILE',size:file.size,status:'Active' as const}));\n      change([...records,...added]);setFiles([]);setOpen(false);setMessage(onImport ? added.length+' resources imported.' : added.length+' metadata records added. File contents were not stored or uploaded.');\n    } catch(cause) {setError(cause instanceof Error ? cause.message : 'Import failed. Try again.');} finally {setBusy(false);}\n  }\n  return <section className=\"bblocks-panel\" aria-label=\"Resource library\"><div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">THE MATERIAL, IN REACH</p><h2>A home for the references.</h2><p className=\"bblocks-muted\">Filter records, export metadata and archive with a way back.</p></div><Dialog title=\"Add resources\" description=\"Select files to add their metadata to the library.\" open={open} onOpenChange={next=>{if(!busy)setOpen(next);}} trigger={<Button leadingIcon={Plus}>Add resources</Button>}>\n    <form className=\"bblocks-stack\" onSubmit={add}><FileUpload label=\"Resource files\" value={files} onValueChange={setFiles} accept=\".pdf,.txt,.png,.jpg\" maxFiles={5} maxSize={10*1024*1024} disabled={busy} />{error && <Alert title=\"Unable to add resources\" tone=\"danger\" live>{error}</Alert>}<p className=\"bblocks-note\">{onImport ? 'Your import callback owns file storage.' : 'This local example saves metadata only. File contents never leave your browser.'}</p><Button type=\"submit\" loading={busy}>Add to library</Button></form>\n  </Dialog></div>\n    <DataTable caption=\"Resources\" rows={records} columns={columns} rowKey={row=>row.id} rowLabel={row=>row.name} searchText={row=>row.name+' '+row.type} pageSize={4} selectable selectedKeys={selected} onSelectedKeysChange={setSelected} columnVisibility density=\"compact\" filters={[{key:'status',label:'Statuses',options:[{value:'Active',label:'Active'},{value:'Archived',label:'Archived'}],value:row=>row.status},{key:'type',label:'Types',options:['PDF','TXT','PNG','JPG'].map(value=>({value,label:value})),value:row=>row.type}]} renderBulkActions={chosen=><><Button leadingIcon={Download} variant=\"secondary\" size=\"sm\" onClick={()=>{download(chosen);setMessage(chosen.length+' metadata records exported.');}}>Export CSV</Button><Button leadingIcon={Archive} size=\"sm\" onClick={()=>{setUndo(chosen.map(row=>({id:row.id,status:row.status})));change(records.map(row=>chosen.some(item=>item.id===row.id) ? {...row,status:'Archived'} : row));setSelected([]);setMessage(chosen.length+' resources archived.');}}>Archive</Button></>} />\n    <div className=\"bblocks-resource-footer\"><p role=\"status\" className=\"bblocks-status\">{message}</p>{undo.length>0 && <Button variant=\"ghost\" size=\"sm\" leadingIcon={Undo2} onClick={()=>{change(records.map(row=>{const previous=undo.find(item=>item.id===row.id);return previous ? {...row,status:previous.status} : row;}));setUndo([]);setMessage('Archive undone.');}}>Undo archive</Button>}</div>\n    <p className=\"bblocks-note\">Illustrative file metadata. No file hosting or storage service is included.</p>\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/sign-in.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { Button, Field, Input, Icon } from '@bld8/ui';\n\nexport function SignInForm({ onSignIn }: { onSignIn?: (email: string, password: string) => Promise<void> }) {\n  const [email, setEmail] = useState('');\n  const [password, setPassword] = useState('');\n  const [errors, setErrors] = useState<{ email?: string; password?: string }>({});\n  const [busy, setBusy] = useState(false);\n  const [message, setMessage] = useState('');\n  async function submit(event: FormEvent) {\n    event.preventDefault();\n    const next = { email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.trim()) ? undefined : 'Enter a valid email address.', password: password.length >= 8 ? undefined : 'Use at least 8 characters.' };\n    setErrors(next); setMessage('');\n    if (next.email || next.password) return;\n    setBusy(true);\n    try {\n      if (onSignIn) await onSignIn(email.trim(), password);\n      setMessage(onSignIn ? 'Sign-in request completed.' : 'Form validated. Connect your authentication service to sign in.');\n      setPassword('');\n    } catch { setMessage('Unable to sign in. Please try again.'); }\n    finally { setBusy(false); }\n  }\n  return <section className=\"bblocks-auth bblocks-panel\" aria-label=\"Sign-in form\">\n    <span className=\"bblocks-symbol\" aria-hidden=\"true\"><Icon name=\"plus\" /></span>\n    <p className=\"bblocks-kicker\">YOUR WORKSPACE</p><h2>Welcome back.</h2><p className=\"bblocks-muted\">A little space to do your best work.</p>\n    <form className=\"bblocks-stack\" onSubmit={submit} noValidate>\n      <Field label=\"Email\" error={errors.email} required><Input type=\"email\" autoComplete=\"username\" value={email} placeholder=\"you@example.com\" onChange={e => setEmail(e.target.value)} /></Field>\n      <Field label=\"Password\" error={errors.password} required><Input type=\"password\" autoComplete=\"current-password\" value={password} onChange={e => setPassword(e.target.value)} /></Field>\n      <Button type=\"submit\" loading={busy}>Continue <Icon /></Button>\n    </form>\n    <p className=\"bblocks-status\" role=\"status\">{message}</p>\n    {!onSignIn && <p className=\"bblocks-note\">Demo form. No account is created and no credentials are sent.</p>}\n  </section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/styles.css",
      "content": ".bblocks-root { container-type:inline-size; container-name:bld8-preview; }\n.bblocks-panel { background:var(--bui-bg); border-radius:var(--bui-panel-radius); padding:clamp(1.25rem,3vw,2rem); }\n.bblocks-stack { display:flex; flex-direction:column; gap:1.5rem; min-width:0; }\n.bblocks-heading { display:flex; justify-content:space-between; align-items:center; gap:1.25rem; margin-bottom:1.5rem; }\n.bblocks-heading:last-child { margin-bottom:0; }\n.bblocks-heading h2,.bblocks-auth h2 { margin:.5rem 0; font-size:clamp(1.5rem,2.6vw,2rem); letter-spacing:-.05em; line-height:1.2; }\n.bblocks-kicker { font:10px/1.5 Consolas,monospace; letter-spacing:.1em; color:var(--bui-muted); }\n.bblocks-muted { color:var(--bui-muted); font-size:.8125rem; }\n.bblocks-note { color:var(--bui-muted); font-size:.6875rem; line-height:1.7; }\n.bblocks-status { min-height:1.7em; color:var(--bui-muted); font-size:.75rem; margin:.75rem 0 0; }\n.bblocks-auth { max-width:420px; margin:auto; }\n.bblocks-auth form { margin-top:2rem; }\n.bblocks-auth .bui-button { width:100%; }\n.bblocks-symbol { display:grid; place-items:center; width:40px; height:40px; background:var(--bui-accent); color:var(--bui-accent-ink); border-radius:6px; margin-bottom:2rem; }\n.bblocks-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; }\n.bblocks-rule { border-block:1px solid var(--bui-line); padding:1.5rem 0; }\n.bblocks-segment { display:flex; gap:4px; flex-wrap:wrap; }\n.bblocks-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; margin:1.5rem 0; }\n.bblocks-stat { padding:1.5rem; background:var(--bui-bg); border-radius:var(--bui-panel-radius); }\n.bblocks-stat>span:first-child { font-size:.75rem; color:var(--bui-muted); display:block; }\n.bblocks-stat>strong { display:block; font-size:clamp(1.8rem,3vw,2.5rem); font-weight:550; line-height:1.2; letter-spacing:-.06em; margin:1rem 0; }\n.bblocks-chart { margin:0; }\n.bblocks-chart figcaption { display:flex; justify-content:space-between; flex-wrap:wrap; gap:.5rem; font-size:.8125rem; }\n.bblocks-bars { display:flex; gap:clamp(.5rem,2vw,1.5rem); height:160px; margin-top:2rem; padding-bottom:24px; }\n.bblocks-bars>div { flex:1; display:flex; flex-direction:column; justify-content:flex-end; position:relative; }\n.bblocks-bars span { display:block; background:var(--bui-primary); border-radius:4px 4px 0 0; transition:height var(--bui-duration-slow) var(--bui-ease); }\n.bblocks-bars>div:nth-last-child(2) span { background:var(--bui-accent); }\n.bblocks-bars small { position:absolute; top:calc(100% + 6px); width:100%; text-align:center; font-size:.625rem; color:var(--bui-muted); }\n.bblocks-activity { list-style:none; margin:1.25rem 0; padding:0; }\n.bblocks-activity li { display:flex; gap:1rem; align-items:center; padding:1rem 0; border-top:1px solid var(--bui-line); }\n.bblocks-activity strong { font-size:.8125rem; font-weight:550; }\n.bblocks-activity p { margin:.2rem 0 0; font-size:.75rem; color:var(--bui-muted); }\n.bblocks-activity time { margin-left:auto; font-size:.625rem; color:var(--bui-muted); white-space:nowrap; }\n.bblocks-person { display:flex; align-items:center; gap:.75rem; }\n.bblocks-person strong { font-size:.8125rem; font-weight:500; }\n.bblocks-person small { display:block; font-size:.6875rem; color:var(--bui-muted); }\n.bblocks-columns { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; }\n.bblocks-lane { background:var(--bui-surface); border-radius:8px; padding:1rem; min-width:0; }\n.bblocks-lane h3 { font-size:.8125rem; letter-spacing:-.02em; }\n.bblocks-lane>.bblocks-heading { margin-bottom:1rem; }\n.bblocks-task { background:var(--bui-bg); border-radius:6px; padding:1rem; margin-bottom:.75rem; }\n.bblocks-task h4 { font-size:.875rem; font-weight:550; line-height:1.5; margin:.5rem 0 1rem; letter-spacing:-.02em; }\n.bblocks-task .bui-label { font-size:.625rem; }\n.bblocks-task .bui-select-trigger { font-size:.6875rem; }\n.bblocks-studio { display:grid; grid-template-columns:180px minmax(0,1fr); background:var(--bui-canvas); border-radius:10px; overflow:hidden; }\n.bblocks-studio-sidebar { background:var(--bui-bg); padding:1.5rem 1rem; }\n.bblocks-studio-sidebar>.bblocks-kicker { margin:3rem .5rem 1rem; }\n.bblocks-studio-sidebar nav { display:flex; flex-direction:column; gap:.4rem; }\n.bblocks-studio-sidebar nav button { justify-content:flex-start; }\n.bblocks-studio-sidebar>.bblocks-note { margin:3rem .5rem 1rem; }\n.bblocks-wordmark { display:inline-flex; align-items:center; gap:.65rem; font-weight:650; font-size:1.125rem; text-decoration:none; letter-spacing:-.04em; }\n.bblocks-wordmark>span { color:var(--bui-ink); background:var(--bui-accent); display:grid; place-items:center; width:28px; height:28px; border-radius:4px; }\n.bblocks-studio-main { padding:1.5rem; min-width:0; }\n.bblocks-studio-top { display:flex; justify-content:space-between; align-items:center; padding-bottom:1.25rem; margin-bottom:2rem; border-bottom:1px solid var(--bui-line); font-size:.8125rem; }\n.bblocks-launch { background:var(--bui-canvas); padding:clamp(1.25rem,4vw,3rem); border-radius:10px; }\n.bblocks-launch>header { display:flex; justify-content:space-between; gap:1rem; align-items:center; }\n.bblocks-launch>header>a:last-child { font-size:.75rem; display:inline-flex; align-items:center; gap:.5rem; text-decoration:none; }\n.bblocks-launch-hero { display:grid; grid-template-columns:1.1fr 1fr; align-items:center; gap:2rem; padding:4rem 0; }\n.bblocks-launch-hero h2 { font-size:clamp(2rem,4vw,3.5rem); line-height:1.08; letter-spacing:-.06em; margin:1.5rem 0; }\n.bblocks-launch-hero p:not(.bblocks-kicker) { font-size:.875rem; color:var(--bui-muted); max-width:340px; margin:1.5rem 0; }\n.bblocks-launch-hero .bblocks-note { display:block; margin-top:1rem; }\n.bblocks-launch-sketch { padding:1.25rem; background:var(--bui-accent); border-radius:8px; color:var(--bui-accent-ink); }\n.bblocks-launch-sketch>div { background:var(--bui-bg); color:var(--bui-ink); padding:1.5rem; border-radius:6px; }\n.bblocks-launch-sketch strong { display:block; font-size:1.75rem; font-weight:550; letter-spacing:-.05em; margin:1rem 0 1.5rem; }\n.bblocks-sketch-row { display:flex; justify-content:space-between; gap:.5rem; align-items:center; border-top:1px solid var(--bui-line); padding:.75rem 0; font-size:.6875rem; }\n.bblocks-launch-features { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1.5rem; padding:2rem 0; border-top:1px solid var(--bui-line); }\n.bblocks-launch-features h3 { font-size:1rem; margin:1rem 0 .5rem; }\n.bblocks-launch-features p { color:var(--bui-muted); font-size:.75rem; }\n.bblocks-launch>footer { display:flex; justify-content:space-between; flex-wrap:wrap; gap:1rem; border-top:1px solid var(--bui-line); padding-top:1.5rem; font-size:.625rem; color:var(--bui-muted); }\n@media(max-width:680px) {\n  .bblocks-heading { flex-wrap:wrap; align-items:flex-start; }\n  .bblocks-form-grid,.bblocks-columns { grid-template-columns:1fr; }\n  .bblocks-stat { padding:1rem; }\n  .bblocks-stat>strong { font-size:1.6rem; }\n  .bblocks-studio { grid-template-columns:1fr; }\n  .bblocks-studio-sidebar { padding:1rem; }\n  .bblocks-studio-sidebar nav { flex-direction:row; flex-wrap:wrap; margin-top:1rem; }\n  .bblocks-studio-sidebar>.bblocks-kicker,.bblocks-studio-sidebar>.bblocks-note { display:none; }\n  .bblocks-studio-main { padding:1rem; }\n  .bblocks-launch-hero,.bblocks-launch-features { grid-template-columns:1fr; }\n  .bblocks-launch-hero { padding:2.5rem 0; }\n  .bblocks-activity time { display:none; }\n}\n@media(max-width:400px) { .bblocks-stats { gap:.5rem; }.bblocks-stat { padding:.75rem; }.bblocks-stat>strong { font-size:1.4rem; } }\n@container bld8-preview (max-width:680px) {\n  .bblocks-heading { flex-wrap:wrap; align-items:flex-start; }\n  .bblocks-form-grid,.bblocks-columns { grid-template-columns:1fr; }\n  .bblocks-stat { padding:1rem; }.bblocks-stat>strong { font-size:1.6rem; }\n  .bblocks-studio { grid-template-columns:1fr; }.bblocks-studio-sidebar { padding:1rem; }\n  .bblocks-studio-sidebar nav { flex-direction:row; flex-wrap:wrap; margin-top:1rem; }\n  .bblocks-studio-sidebar>.bblocks-kicker,.bblocks-studio-sidebar>.bblocks-note { display:none; }.bblocks-studio-main { padding:1rem; }\n  .bblocks-launch-hero,.bblocks-launch-features { grid-template-columns:1fr; }.bblocks-launch-hero { padding:2.5rem 0; }.bblocks-activity time { display:none; }\n}\n@media(prefers-reduced-motion:reduce) { .bblocks-bars span { transition:none; } }\n.bblocks-date { display:inline-flex; align-items:center; gap:8px; }\n.bblocks-tags { display:inline-flex; gap:4px; flex-wrap:wrap; }\n.bblocks-planner-summary { display:flex; flex-wrap:wrap; gap:24px; padding:16px 0; margin-bottom:16px; border-block:1px solid var(--bui-line); color:var(--bui-muted); font-size:.75rem; }\n.bblocks-planner-summary strong { color:var(--bui-ink); font-size:1rem; margin-right:6px; }\n.bblocks-resource-footer { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px; margin-block:12px; }\n.bblocks-operations { background:var(--bui-canvas); padding:clamp(16px,4vw,36px); border-radius:10px; }\n.bblocks-operations-header { display:flex; align-items:center; gap:16px; flex-wrap:wrap; padding-bottom:24px; border-bottom:1px solid var(--bui-line); }\n.bblocks-operations-header>.bui-button { margin-left:auto; }\n.bblocks-operations-intro { padding:36px 0 24px; }\n.bblocks-operations-intro h2 { font-size:clamp(1.75rem,4vw,2.5rem); font-weight:500; line-height:1.2; letter-spacing:-.05em; margin:12px 0; outline:none; }\n.bblocks-operations-nav { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:20px; }\n.bblocks-operations>footer { margin-top:24px; }\n@container bld8-preview (max-width:550px) { .bblocks-panel>.bblocks-heading { flex-direction:column; align-items:flex-start; } .bblocks-operations-header>.bui-badge { display:none; } }\n.bblocks-operations-layout { margin-top:24px; align-items:stretch; }\n.bblocks-operations-toolbar { display:flex; align-items:center; gap:12px; font-size:.75rem; }\n.bblocks-operations-layout .bblocks-operations-intro { padding:20px 0 24px; }\n\n.bblocks-inline-actions {display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n.bblocks-project-name {white-space:normal;text-align:start;justify-content:flex-start;max-width:100%}\n\n.bblocks-table-options>.bui-field {width:180px;max-width:100%;flex:0 0 auto}\n.bblocks-project-name {padding-inline:0}\n"
    },
    {
      "path": "packages/blocks/src/team.tsx",
      "content": "'use client';\nimport { useState, type FormEvent } from 'react';\nimport { Avatar, Badge, Button, DataTable, Dialog, Field, Input, Select, type Column } from '@bld8/ui';\n\nexport interface TeamMember { id:string; name:string; email:string; role:string; status:'Active' | 'Invited' }\nconst sample: TeamMember[] = [\n  {id:'1',name:'Maya Chen',email:'maya@example.com',role:'Admin',status:'Active'},\n  {id:'2',name:'Oliver Reed',email:'oliver@example.com',role:'Editor',status:'Active'},\n  {id:'3',name:'Sam Rivera',email:'sam@example.com',role:'Editor',status:'Active'},\n  {id:'4',name:'Avery Brooks',email:'avery@example.com',role:'Viewer',status:'Active'},\n];\nexport function TeamDirectory({ initialMembers = sample }: { initialMembers?: TeamMember[] }) {\n  const [members,setMembers] = useState(initialMembers);\n  const [open,setOpen] = useState(false);\n  const [email,setEmail] = useState('');\n  const [role,setRole] = useState('Editor');\n  const [error,setError] = useState('');\n  const [message,setMessage] = useState('');\n  const columns: Column<TeamMember>[] = [\n    {key:'name',header:'Person',sortValue:row => row.name,cell:row => <div className=\"bblocks-person\"><Avatar name={row.name} /><div><strong>{row.name}</strong><small>{row.email}</small></div></div>},\n    {key:'role',header:'Role',sortValue:row => row.role,cell:row => row.role},\n    {key:'status',header:'Status',cell:row => <Badge tone={row.status === 'Active' ? 'success' : 'accent'}>{row.status}</Badge>},\n  ];\n  function invite(event: FormEvent) {\n    event.preventDefault(); const address = email.trim();\n    if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(address)) { setError('Enter a valid email address.'); return; }\n    if (members.some(member => member.email.toLowerCase() === address.toLowerCase())) { setError('This email is already in the workspace.'); return; }\n    setMembers(current => [...current,{id:crypto.randomUUID(),name:address.split('@')[0],email:address,role,status:'Invited'}]);\n    setOpen(false); setEmail(''); setError(''); setMessage('Invitation added to this demo. No email was sent.');\n  }\n  return <section className=\"bblocks-panel\" aria-label=\"Team directory\"><div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">PEOPLE & ACCESS</p><h2>Better, together.</h2><p className=\"bblocks-muted\">{members.length} people in this sample workspace.</p></div><Dialog open={open} onOpenChange={next => {setOpen(next);setError('');}} title=\"Invite a teammate\" description=\"Add a person to this sample workspace. No invitation email is sent.\" trigger={<Button>Invite person</Button>}><form className=\"bblocks-stack\" onSubmit={invite} noValidate><Field label=\"Invite email\" error={error} required><Input type=\"email\" value={email} onChange={e => setEmail(e.target.value)} /></Field><Select label=\"Workspace role\" options={['Admin','Editor','Viewer'].map(value => ({value,label:value}))} value={role} onValueChange={value => { if (value) setRole(value); }} /><Button type=\"submit\">Add invitation</Button></form></Dialog></div><DataTable caption=\"Team members\" rows={members} columns={columns} rowKey={row => row.id} searchText={row => row.name + ' ' + row.email + ' ' + row.role} pageSize={5} /><p className=\"bblocks-status\" role=\"status\">{message}</p></section>;\n}\n"
    },
    {
      "path": "packages/blocks/src/templates.tsx",
      "content": "'use client';\nimport { useId, useState } from 'react';\nimport { Badge, Button, Dialog, Icon } from '@bld8/ui';\nimport { AccountSettings } from './account-settings.js';\nimport { SignInForm } from './sign-in.js';\nimport { ProjectBoard } from './project-board.js';\nimport { TeamDirectory } from './team.js';\nimport { WorkspaceOverview } from './workspace.js';\n\nexport function StudioTemplate() {\n  const [view,setView] = useState('Overview');\n  return <div className=\"bblocks-studio\"><aside className=\"bblocks-studio-sidebar\"><a href=\"#\" onClick={event => {event.preventDefault();setView('Overview');}} className=\"bblocks-wordmark\"><span aria-hidden=\"true\">✳</span> Studio</a><p className=\"bblocks-kicker\">YOUR WORKSPACE</p><nav aria-label=\"Studio sections\">{['Overview','Projects','People','Settings'].map(label => <Button key={label} variant={label === view ? 'primary' : 'ghost'} aria-pressed={label === view} onClick={() => setView(label)}>{label}</Button>)}</nav><p className=\"bblocks-note\">A working interface starter.<br />All data is illustrative.</p></aside><div className=\"bblocks-studio-main\"><div className=\"bblocks-studio-top\"><span>{view}</span><Badge tone=\"accent\">Sample workspace</Badge></div>{view === 'Overview' && <WorkspaceOverview />}{view === 'Projects' && <ProjectBoard />}{view === 'People' && <TeamDirectory />}{view === 'Settings' && <AccountSettings />}</div></div>;\n}\nexport function LaunchTemplate() {\n  const prefix = useId();\n  return <div className=\"bblocks-launch\"><header><a className=\"bblocks-wordmark\" href={'#' + prefix + '-top'}><span aria-hidden=\"true\">✳</span> Gather</a><a href={'#' + prefix + '-details'}>The details <Icon /></a></header><section className=\"bblocks-launch-hero\" id={prefix + '-top'}><div><p className=\"bblocks-kicker\">LESS NOISE. MORE ROOM.</p><h2>A calmer place<br />for your next idea.</h2><p>Bring the work, people and small decisions into one clear workspace.</p><Dialog title=\"Try the sign-in form\" description=\"This starter includes an interface, not an authentication service.\" trigger={<Button>Try the demo <Icon /></Button>}><SignInForm /></Dialog><span className=\"bblocks-note\">An illustrative product, built with BLD8 UI.</span></div><div className=\"bblocks-launch-sketch\" aria-hidden=\"true\"><div><span className=\"bblocks-kicker\">THE WEEK, AT A GLANCE</span><strong>A little progress.</strong><div className=\"bblocks-sketch-row\"><span>Navigation study</span><Badge tone=\"accent\">In progress</Badge></div><div className=\"bblocks-sketch-row\"><span>First ideas</span><Badge tone=\"success\">Done</Badge></div><div className=\"bblocks-sketch-row\"><span>Next conversation</span><Badge>Planned</Badge></div></div></div></section><section className=\"bblocks-launch-features\" id={prefix + '-details'}>{[['01','Keep the work in view.','A clear overview without the clutter.'],['02','Make room for people.','A shared space with considered controls.'],['03','Let the details settle.','Typography, spacing and motion in a common rhythm.']].map(([number,title,copy]) => <article key={number}><span className=\"bblocks-kicker\">{number} / THE DETAILS</span><h3>{title}</h3><p>{copy}</p></article>)}</section><footer>Gather / an interface starter <span>Made with BLD8 UI</span></footer></div>;\n}\n"
    },
    {
      "path": "packages/blocks/src/workspace.tsx",
      "content": "'use client';\nimport { useState } from 'react';\nimport { Avatar, Badge, Button } from '@bld8/ui';\nimport { MetricsOverview } from './metrics.js';\n\nconst activity = [\n  { name:'Maya Chen', action:'updated the navigation study', time:'12 min ago', kind:'Updates' },\n  { name:'Oliver Reed', action:'recorded a typography decision', time:'38 min ago', kind:'Decisions' },\n  { name:'Sam Rivera', action:'shared the first dashboard draft', time:'1 hour ago', kind:'Updates' },\n];\nexport function WorkspaceOverview() {\n  const [filter,setFilter] = useState('All');\n  const items = activity.filter(item => filter === 'All' || item.kind === filter);\n  return <div className=\"bblocks-stack\"><MetricsOverview /><section className=\"bblocks-panel\" aria-label=\"Workspace activity\"><div className=\"bblocks-heading\"><div><p className=\"bblocks-kicker\">IN THE WORKSPACE</p><h2>Small steps, together.</h2></div><Badge>Sample data</Badge></div><div className=\"bblocks-segment\" role=\"group\" aria-label=\"Activity filter\">{['All','Updates','Decisions'].map(label => <Button key={label} size=\"sm\" variant={filter === label ? 'primary' : 'ghost'} aria-pressed={filter === label} onClick={() => setFilter(label)}>{label}</Button>)}</div><ul className=\"bblocks-activity\">{items.map(item => <li key={item.name}><Avatar name={item.name} /><div><strong>{item.name}</strong><p>{item.action}</p></div><time>{item.time}</time></li>)}</ul><p className=\"bblocks-note\" role=\"status\">{items.length} activity {items.length === 1 ? 'item' : 'items'}. Illustrative records, not live analytics.</p></section></div>;\n}\n"
    }
  ],
  "download": "/downloads/resources.zip"
}