@usevyre/ai-context 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- // @usevyre/ai-context v1.1.0
1
+ // @usevyre/ai-context v1.2.0
2
2
  // Auto-generated — do not edit directly. Edit src/schema/components.json instead.
3
3
 
4
- export const version = "1.1.0";
4
+ export const version = "1.2.0";
5
5
 
6
6
  export const fullContext = `# useVyre Design System — AI Context
7
7
  # Version: 0.2.0
@@ -310,7 +310,7 @@ import { Button } from "@usevyre/react"
310
310
 
311
311
  ### Calendar
312
312
 
313
- Date picker calendar widget for selecting single dates or ranges.
313
+ Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.
314
314
 
315
315
  \`\`\`tsx
316
316
  import { Calendar } from "@usevyre/react"
@@ -319,6 +319,7 @@ import { Calendar } from "@usevyre/react"
319
319
  // value = Date | null
320
320
  // onChange = function
321
321
  // disabled = boolean (default: false)
322
+ // defaultMonth = Date
322
323
 
323
324
  // Examples:
324
325
  const [date, setDate] = useState(null);
@@ -326,7 +327,39 @@ const [date, setDate] = useState(null);
326
327
  \`\`\`
327
328
 
328
329
  **Common mistakes:**
329
- - ❌ \`Using Calendar for time selection\` Combine with a separate time Input if time selection is needed
330
+ - ❌ \`Calendar for an input field that opens a popover\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
331
+ - ❌ \`value as tuple for mode="single"\` → Pass value matching mode; use mode="range" for [start,end]
332
+
333
+ ---
334
+
335
+ ### DatePicker
336
+
337
+ Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.
338
+
339
+ \`\`\`tsx
340
+ import { DatePicker } from "@usevyre/react"
341
+
342
+ // Props:
343
+ // value = Date | [Date, Date] | Date[] | null
344
+ // onChange = function
345
+ // mode = "single" | "range" | "multiple" (default: single)
346
+ // placeholder = string (default: Pick a date)
347
+ // showTime = boolean (default: false)
348
+ // minDate = Date
349
+ // maxDate = Date
350
+ // disabled = function
351
+ // weekStartsOn = "0" | "1" (default: 1)
352
+ // inputClassName = string
353
+
354
+ // Examples:
355
+ const [date, setDate] = useState(null);
356
+ <DatePicker value={date} onChange={setDate} placeholder="Pick a date" />
357
+ <DatePicker value={date} onChange={setDate} showTime />
358
+ \`\`\`
359
+
360
+ **Common mistakes:**
361
+ - ❌ \`DatePicker mode="range" for { from, to } object\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
362
+ - ❌ \`DatePicker without value/onChange\` → Provide value and onChange (e.g. from useState)
330
363
 
331
364
  ---
332
365
 
@@ -386,6 +419,78 @@ import { Checkbox } from "@usevyre/react"
386
419
 
387
420
  ---
388
421
 
422
+ ### RadioGroup
423
+
424
+ Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.
425
+
426
+ \`\`\`tsx
427
+ import { RadioGroup, Radio } from "@usevyre/react"
428
+
429
+ // Props:
430
+ // value = string
431
+ // defaultValue = string
432
+ // onChange = function
433
+ // name = string
434
+ // disabled = boolean (default: false)
435
+ // size = "sm" | "md" (default: md)
436
+ // orientation = "vertical" | "horizontal" (default: vertical)
437
+ // options = { value: string; label?: string; description?: string; disabled?: boolean }[]
438
+
439
+ // Examples:
440
+ <RadioGroup
441
+ value={plan}
442
+ onChange={setPlan}
443
+ options={[
444
+ { value: "free", label: "Free", description: "For hobby projects" },
445
+ { value: "pro", label: "Pro", description: "For teams" },
446
+ ]}
447
+ />
448
+ <RadioGroup value={plan} onChange={setPlan} orientation="horizontal">
449
+ <Radio value="free" label="Free" />
450
+ <Radio value="pro" label="Pro" />
451
+ </RadioGroup>
452
+ \`\`\`
453
+
454
+ **Common mistakes:**
455
+ - ❌ \`<Radio> used outside a <RadioGroup>\` → Always wrap <Radio> in <RadioGroup>
456
+ - ❌ \`RadioGroup without value/onChange (React) or v-model (Vue)\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
457
+ - ❌ \`Using Checkbox for mutually-exclusive choices\` → Use RadioGroup + Radio (or options) for one-of-many
458
+
459
+ ---
460
+
461
+ ### RichTextEditor
462
+
463
+ Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.
464
+
465
+ \`\`\`tsx
466
+ import { RichTextEditor } from "@usevyre/react"
467
+
468
+ // Props:
469
+ // value = string
470
+ // onChange = function
471
+ // placeholder = string (default: Write something…)
472
+ // disabled = boolean (default: false)
473
+ // readOnly = boolean (default: false)
474
+ // toolbar = RichTextTool[]
475
+ // minHeight = string (default: 10rem)
476
+
477
+ // Examples:
478
+ const [html, setHtml] = useState("<p>Hello <strong>world</strong></p>");
479
+ <RichTextEditor value={html} onChange={setHtml} placeholder="Write…" />
480
+ <RichTextEditor
481
+ value={html}
482
+ onChange={setHtml}
483
+ toolbar={["bold", "italic", "link"]}
484
+ />
485
+ \`\`\`
486
+
487
+ **Common mistakes:**
488
+ - ❌ \`RichTextEditor without value/onChange (React) or v-model (Vue)\` → Keep the HTML string in state and update it in onChange / v-model
489
+ - ❌ \`Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
490
+ - ❌ \`toolbar="bold" (string)\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
491
+
492
+ ---
493
+
389
494
  ### Command
390
495
 
391
496
  Command palette / search dialog. Use for search-first navigation or quick actions.
@@ -437,7 +542,7 @@ import { DropdownMenu, DropdownItem, DropdownSeparator, DropdownCheckboxItem, Dr
437
542
 
438
543
  ### Field
439
544
 
440
- Form field wrapper providing label, hint text, and validation state for Input or Textarea.
545
+ Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.
441
546
 
442
547
  \`\`\`tsx
443
548
  import { Field, Input, Textarea } from "@usevyre/react"
@@ -455,10 +560,23 @@ import { Field, Input, Textarea } from "@usevyre/react"
455
560
  <Field label="Search">
456
561
  <Input leftElement={<SearchIcon />} placeholder="Search..." />
457
562
  </Field>
563
+ <Field>
564
+ <FieldLabel required htmlFor="email">Email</FieldLabel>
565
+ <Input id="email" type="email" />
566
+ <FieldDescription>We\u2019ll never share it.</FieldDescription>
567
+ <FieldError>{errors.email}</FieldError>
568
+ </Field>
569
+
570
+ // Two controls side by side
571
+ <FieldGroup orientation="horizontal">
572
+ <Field label="First name"><Input /></Field>
573
+ <Field label="Last name"><Input /></Field>
574
+ </FieldGroup>
458
575
  \`\`\`
459
576
 
460
577
  **Common mistakes:**
461
578
  - ❌ \`Applying state prop directly to Input\` → Wrap Input in <Field state="error"> to apply validation styling
579
+ - ❌ \`Mixing props label/hint AND FieldLabel/FieldError for the same field\` → Pick one: either props-based (label/hint/state) OR composable parts
462
580
 
463
581
  ---
464
582
 
@@ -470,6 +588,7 @@ Text input field. Wrap in Field for labels and validation. Use leftElement/right
470
588
  import { Input } from "@usevyre/react"
471
589
 
472
590
  // Props:
591
+ // modelValue = string | number
473
592
  // size = "sm" | "md" | "lg" (default: md)
474
593
  // leftElement = ReactNode
475
594
  // rightElement = ReactNode
@@ -481,6 +600,7 @@ import { Input } from "@usevyre/react"
481
600
  **Common mistakes:**
482
601
  - ❌ \`size="icon"\` → Use size="sm" | "md" | "lg"
483
602
  - ❌ \`type="search" for search UI\` → Import Command from @usevyre/react for search palettes
603
+ - ❌ \`Vue: binding Input/Textarea value without v-model\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
484
604
 
485
605
  ---
486
606
 
@@ -1068,6 +1188,180 @@ import { TagGroup, Tag } from "@usevyre/react"
1068
1188
 
1069
1189
  ---
1070
1190
 
1191
+ ### Item
1192
+
1193
+ Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.
1194
+
1195
+ \`\`\`tsx
1196
+ import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from "@usevyre/react"
1197
+
1198
+ // Props:
1199
+ // variant = "default" | "outlined" | "muted" | "plain" (default: default)
1200
+ // size = "sm" | "md" | "lg" (default: md)
1201
+ // clickable = boolean (default: false)
1202
+
1203
+ // Examples:
1204
+ <Item>
1205
+ <ItemMedia><BellIcon /></ItemMedia>
1206
+ <ItemContent>
1207
+ <ItemTitle>Notifications</ItemTitle>
1208
+ <ItemDescription>Receive an email when someone mentions you.</ItemDescription>
1209
+ </ItemContent>
1210
+ <ItemActions>
1211
+ <Switch defaultChecked />
1212
+ </ItemActions>
1213
+ </Item>
1214
+ <ItemGroup separated>
1215
+ <Item clickable>
1216
+ <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>
1217
+ </Item>
1218
+ <Item clickable>
1219
+ <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>
1220
+ </Item>
1221
+ </ItemGroup>
1222
+ \`\`\`
1223
+
1224
+ **Common mistakes:**
1225
+ - ❌ \`Card used for repeated list rows\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
1226
+ - ❌ \`Item variant="primary"\` → Use variant="default" | "outlined" | "muted"
1227
+ - ❌ \`raw text directly inside Item\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
1228
+
1229
+ ---
1230
+
1231
+ ### Kanban
1232
+
1233
+ Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant="outlined"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.
1234
+
1235
+ \`\`\`tsx
1236
+ import { Kanban } from "@usevyre/react"
1237
+
1238
+ // Props:
1239
+ // value = KanbanColumn[]
1240
+ // onChange = function
1241
+ // renderCard = function
1242
+ // onCardClick = function
1243
+
1244
+ // Examples:
1245
+ const [columns, setColumns] = useState([
1246
+ { id: "todo", title: "To Do", cards: [{ id: "1", title: "Spec API" }] },
1247
+ { id: "doing", title: "In Progress", cards: [] },
1248
+ { id: "done", title: "Done", cards: [{ id: "2", title: "Kickoff" }] },
1249
+ ]);
1250
+ <Kanban value={columns} onChange={setColumns} />
1251
+ <Kanban
1252
+ value={columns}
1253
+ onChange={setColumns}
1254
+ onCardClick={(card) => openDetail(card.id)}
1255
+ renderCard={(card) => (
1256
+ <><strong>{card.title}</strong><Badge>{card.id}</Badge></>
1257
+ )}
1258
+ />
1259
+ const [cols, setCols] = useState([
1260
+ { id: "doing", title: "In Progress", color: "teal", cards: [
1261
+ { id: "t1", title: "OAuth", assignee: "AK", progress: 60, color: "warning" },
1262
+ ]},
1263
+ ]);
1264
+ <Kanban
1265
+ value={cols}
1266
+ onChange={setCols}
1267
+ renderCard={(card) => (
1268
+ <><strong>{card.title}</strong><Progress value={card.progress} /></>
1269
+ )}
1270
+ />
1271
+ \`\`\`
1272
+
1273
+ **Common mistakes:**
1274
+ - ❌ \`Kanban without onChange (or ignoring it)\` → Store columns in state and setColumns in onChange (v-model in Vue)
1275
+ - ❌ \`Duplicate card ids across columns\` → Use globally-unique card ids across the entire board
1276
+ - ❌ \`Mutating value in place then calling onChange\` → Pass the new array Kanban gives you straight to setState / v-model
1277
+ - ❌ \`color="blue" (or any non-semantic value)\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
1278
+
1279
+ ---
1280
+
1281
+ ### Conversation
1282
+
1283
+ Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own \`value\` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.
1284
+
1285
+ \`\`\`tsx
1286
+ import { Conversation } from "@usevyre/react"
1287
+
1288
+ // Props:
1289
+ // value = ConversationMessage[]
1290
+ // currentUserId = string
1291
+ // composer = boolean (default: false)
1292
+ // onSend = function
1293
+ // placeholder = string (default: Write a message…)
1294
+ // typing = boolean | string (default: false)
1295
+ // allowAttachments = boolean (default: false)
1296
+ // accept = string
1297
+ // renderMessage = function
1298
+ // renderComposer = function
1299
+
1300
+ // Examples:
1301
+ const [messages, setMessages] = useState([
1302
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Hey!" },
1303
+ { id: "2", authorId: "me", text: "Hi \ud83d\udc4b", status: "read" },
1304
+ ]);
1305
+ <Conversation
1306
+ value={messages}
1307
+ currentUserId="me"
1308
+ composer
1309
+ onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: "me", text: t }])}
1310
+ />
1311
+ <Conversation
1312
+ value={messages}
1313
+ currentUserId="me"
1314
+ typing="Sam is typing"
1315
+ renderMessage={(m) => <strong>{m.text}</strong>}
1316
+ />
1317
+ const messages = [
1318
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Moodboard \ud83d\udc47",
1319
+ attachments: [{ kind: "image", url: "/board.png", name: "board.png" }] },
1320
+ { id: "2", authorId: "me", text: "Specs:", status: "read",
1321
+ attachments: [{ kind: "file", url: "/spec.pdf", name: "spec.pdf", size: "2.4 MB" }] },
1322
+ ];
1323
+ <Conversation value={messages} currentUserId="me" />
1324
+ \`\`\`
1325
+
1326
+ **Common mistakes:**
1327
+ - ❌ \`Conversation without currentUserId\` → Always pass currentUserId matching one of the message authorId values
1328
+ - ❌ \`Expecting Conversation to store/append messages\` → Append to your own state in onSend (or @send) and pass it back via value
1329
+ - ❌ \`composer without onSend (React) / @send (Vue)\` → Provide onSend / @send to append the message to value
1330
+ - ❌ \`Treating onSend as (text) only when using allowAttachments\` → Handle onSend(text, files) — map files to message attachments and append
1331
+
1332
+ ---
1333
+
1334
+ ### DateRangePicker
1335
+
1336
+ Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.
1337
+
1338
+ \`\`\`tsx
1339
+ import { DateRangePicker } from "@usevyre/react"
1340
+
1341
+ // Props:
1342
+ // value = { from: Date | null; to: Date | null } | null
1343
+ // onChange = function
1344
+ // placeholder = string (default: Pick a date range)
1345
+ // numberOfMonths = "1" | "2" (default: 2)
1346
+ // presets = boolean | DateRangePreset[] (default: false)
1347
+ // minDate = Date
1348
+ // maxDate = Date
1349
+ // disabled = function
1350
+ // weekStartsOn = "0" | "1" (default: 1)
1351
+
1352
+ // Examples:
1353
+ const [range, setRange] = useState({ from: null, to: null });
1354
+ <DateRangePicker value={range} onChange={setRange} presets />
1355
+ <DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />
1356
+ \`\`\`
1357
+
1358
+ **Common mistakes:**
1359
+ - ❌ \`value={[from, to]}\` → Use value={{ from, to }} and read range.from / range.to
1360
+ - ❌ \`DateRangePicker for a single date\` → Use <DatePicker /> for a single date
1361
+ - ❌ \`presets="true" (string)\` → Use the bare prop: presets (or presets={true})
1362
+
1363
+ ---
1364
+
1071
1365
  ## Hallucination Guard — Common AI Mistakes
1072
1366
 
1073
1367
  The following prop values and patterns do NOT exist in useVyre.
@@ -1087,14 +1381,25 @@ If you generate these, you are hallucinating.
1087
1381
  - ❌ \`<Button color="...">\` → Use variant prop instead
1088
1382
  - ❌ \`<Button icon={...}>\` → Use leftIcon={...} or rightIcon={...}
1089
1383
  - ❌ \`<Button size="icon" without aria-label>\` → Add aria-label describing the action
1090
- - ❌ \`<Calendar Using Calendar for time selection>\` Combine with a separate time Input if time selection is needed
1384
+ - ❌ \`<Calendar Calendar for an input field that opens a popover>\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
1385
+ - ❌ \`<Calendar value as tuple for mode="single">\` → Pass value matching mode; use mode="range" for [start,end]
1386
+ - ❌ \`<DatePicker DatePicker mode="range" for { from, to } object>\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
1387
+ - ❌ \`<DatePicker DatePicker without value/onChange>\` → Provide value and onChange (e.g. from useState)
1091
1388
  - ❌ \`<Card variant="primary">\` → Use variant="elevated" | "outlined" | "ghost" | "accent"
1092
1389
  - ❌ \`<Checkbox size="lg">\` → Use size="md"
1390
+ - ❌ \`<RadioGroup <Radio> used outside a <RadioGroup>>\` → Always wrap <Radio> in <RadioGroup>
1391
+ - ❌ \`<RadioGroup RadioGroup without value/onChange (React) or v-model (Vue)>\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
1392
+ - ❌ \`<RadioGroup Using Checkbox for mutually-exclusive choices>\` → Use RadioGroup + Radio (or options) for one-of-many
1393
+ - ❌ \`<RichTextEditor RichTextEditor without value/onChange (React) or v-model (Vue)>\` → Keep the HTML string in state and update it in onChange / v-model
1394
+ - ❌ \`<RichTextEditor Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising>\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
1395
+ - ❌ \`<RichTextEditor toolbar="bold" (string)>\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
1093
1396
  - ❌ \`<Command Using Input type="search" for search UI>\` → Use Command + CommandInput + CommandList + CommandItem
1094
1397
  - ❌ \`<DropdownMenu DropdownItem variant="primary">\` → Use variant="danger" for destructive items only
1095
1398
  - ❌ \`<Field Applying state prop directly to Input>\` → Wrap Input in <Field state="error"> to apply validation styling
1399
+ - ❌ \`<Field Mixing props label/hint AND FieldLabel/FieldError for the same field>\` → Pick one: either props-based (label/hint/state) OR composable parts
1096
1400
  - ❌ \`<Input size="icon">\` → Use size="sm" | "md" | "lg"
1097
1401
  - ❌ \`<Input type="search" for search UI>\` → Import Command from @usevyre/react for search palettes
1402
+ - ❌ \`<Input Vue: binding Input/Textarea value without v-model>\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
1098
1403
  - ❌ \`<Modal size="xl">\` → Use size="lg" or size="full"
1099
1404
  - ❌ \`<Popover placement="top-center">\` → Use placement="top" for centered placement
1100
1405
  - ❌ \`<Progress value > 100>\` → Normalize your value to 0–100 range before passing
@@ -1119,6 +1424,20 @@ If you generate these, you are hallucinating.
1119
1424
  - ❌ \`<Tag Tag size="xl">\` → Use size="lg"
1120
1425
  - ❌ \`<TagGroup TagGroup without Tag children>\` → Place <Tag> elements as direct children
1121
1426
  - ❌ \`<TagGroup Using TagGroup for tag input>\` → Use TagsInput for an editable tag field
1427
+ - ❌ \`<Item Card used for repeated list rows>\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
1428
+ - ❌ \`<Item Item variant="primary">\` → Use variant="default" | "outlined" | "muted"
1429
+ - ❌ \`<Item raw text directly inside Item>\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
1430
+ - ❌ \`<Kanban Kanban without onChange (or ignoring it)>\` → Store columns in state and setColumns in onChange (v-model in Vue)
1431
+ - ❌ \`<Kanban Duplicate card ids across columns>\` → Use globally-unique card ids across the entire board
1432
+ - ❌ \`<Kanban Mutating value in place then calling onChange>\` → Pass the new array Kanban gives you straight to setState / v-model
1433
+ - ❌ \`<Kanban color="blue" (or any non-semantic value)>\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
1434
+ - ❌ \`<Conversation Conversation without currentUserId>\` → Always pass currentUserId matching one of the message authorId values
1435
+ - ❌ \`<Conversation Expecting Conversation to store/append messages>\` → Append to your own state in onSend (or @send) and pass it back via value
1436
+ - ❌ \`<Conversation composer without onSend (React) / @send (Vue)>\` → Provide onSend / @send to append the message to value
1437
+ - ❌ \`<Conversation Treating onSend as (text) only when using allowAttachments>\` → Handle onSend(text, files) — map files to message attachments and append
1438
+ - ❌ \`<DateRangePicker value={[from, to]}>\` → Use value={{ from, to }} and read range.from / range.to
1439
+ - ❌ \`<DateRangePicker DateRangePicker for a single date>\` → Use <DatePicker /> for a single date
1440
+ - ❌ \`<DateRangePicker presets="true" (string)>\` → Use the bare prop: presets (or presets={true})
1122
1441
 
1123
1442
  ---
1124
1443
 
@@ -1173,7 +1492,7 @@ alwaysApply: true
1173
1492
  ---
1174
1493
 
1175
1494
  # useVyre Design System — Cursor Rules
1176
- # Version: 1.1.0
1495
+ # Version: 1.2.0
1177
1496
 
1178
1497
  You are working in a project using the useVyre design system (@usevyre/react).
1179
1498
  Follow these rules strictly when generating any UI code.
@@ -1252,13 +1571,26 @@ Never do:
1252
1571
  - ❌ size="icon" without aria-label → ✅ Add aria-label describing the action
1253
1572
 
1254
1573
  ## Calendar
1255
- Date picker calendar widget for selecting single dates or ranges.
1574
+ Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.
1256
1575
  Import: \`import { Calendar } from "@usevyre/react"\`
1257
1576
 
1258
1577
  Valid props:
1259
1578
 
1260
1579
  Never do:
1261
- - ❌ Using Calendar for time selection → ✅ Combine with a separate time Input if time selection is needed
1580
+ - ❌ Calendar for an input field that opens a popover → ✅ Use <DatePicker /> (single date) or <DateRangePicker /> (range)
1581
+ - ❌ value as tuple for mode="single" → ✅ Pass value matching mode; use mode="range" for [start,end]
1582
+
1583
+ ## DatePicker
1584
+ Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.
1585
+ Import: \`import { DatePicker } from "@usevyre/react"\`
1586
+
1587
+ Valid props:
1588
+ - mode: "single" | "range" | "multiple" [default: single]
1589
+ - weekStartsOn: "0" | "1" [default: 1]
1590
+
1591
+ Never do:
1592
+ - ❌ DatePicker mode="range" for { from, to } object → ✅ Use <DateRangePicker /> for the { from, to } object API + presets + dual month
1593
+ - ❌ DatePicker without value/onChange → ✅ Provide value and onChange (e.g. from useState)
1262
1594
 
1263
1595
  ## Card
1264
1596
  Content container with optional header, body, and footer sections.
@@ -1280,6 +1612,30 @@ Valid props:
1280
1612
  Never do:
1281
1613
  - ❌ size="lg" → ✅ Use size="md"
1282
1614
 
1615
+ ## RadioGroup
1616
+ Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.
1617
+ Import: \`import { RadioGroup, Radio } from "@usevyre/react"\`
1618
+
1619
+ Valid props:
1620
+ - size: "sm" | "md" [default: md]
1621
+ - orientation: "vertical" | "horizontal" [default: vertical]
1622
+
1623
+ Never do:
1624
+ - ❌ <Radio> used outside a <RadioGroup> → ✅ Always wrap <Radio> in <RadioGroup>
1625
+ - ❌ RadioGroup without value/onChange (React) or v-model (Vue) → ✅ Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
1626
+ - ❌ Using Checkbox for mutually-exclusive choices → ✅ Use RadioGroup + Radio (or options) for one-of-many
1627
+
1628
+ ## RichTextEditor
1629
+ Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.
1630
+ Import: \`import { RichTextEditor } from "@usevyre/react"\`
1631
+
1632
+ Valid props:
1633
+
1634
+ Never do:
1635
+ - ❌ RichTextEditor without value/onChange (React) or v-model (Vue) → ✅ Keep the HTML string in state and update it in onChange / v-model
1636
+ - ❌ Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising → ✅ Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
1637
+ - ❌ toolbar="bold" (string) → ✅ Pass an array, e.g. toolbar={["bold","italic","link"]}
1638
+
1283
1639
  ## Command
1284
1640
  Command palette / search dialog. Use for search-first navigation or quick actions.
1285
1641
  Import: \`import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandDialog } from "@usevyre/react"\`
@@ -1297,7 +1653,7 @@ Never do:
1297
1653
  - ❌ DropdownItem variant="primary" → ✅ Use variant="danger" for destructive items only
1298
1654
 
1299
1655
  ## Field
1300
- Form field wrapper providing label, hint text, and validation state for Input or Textarea.
1656
+ Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.
1301
1657
  Import: \`import { Field, Input, Textarea } from "@usevyre/react"\`
1302
1658
 
1303
1659
  Valid props:
@@ -1305,6 +1661,7 @@ Valid props:
1305
1661
 
1306
1662
  Never do:
1307
1663
  - ❌ Applying state prop directly to Input → ✅ Wrap Input in <Field state="error"> to apply validation styling
1664
+ - ❌ Mixing props label/hint AND FieldLabel/FieldError for the same field → ✅ Pick one: either props-based (label/hint/state) OR composable parts
1308
1665
 
1309
1666
  ## Input
1310
1667
  Text input field. Wrap in Field for labels and validation. Use leftElement/rightElement for icons.
@@ -1316,6 +1673,7 @@ Valid props:
1316
1673
  Never do:
1317
1674
  - ❌ size="icon" → ✅ Use size="sm" | "md" | "lg"
1318
1675
  - ❌ type="search" for search UI → ✅ Import Command from @usevyre/react for search palettes
1676
+ - ❌ Vue: binding Input/Textarea value without v-model → ✅ Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
1319
1677
 
1320
1678
  ## Label
1321
1679
  Accessible form label. Associate with input via htmlFor.
@@ -1523,13 +1881,63 @@ Never do:
1523
1881
  - ❌ TagGroup without Tag children → ✅ Place <Tag> elements as direct children
1524
1882
  - ❌ Using TagGroup for tag input → ✅ Use TagsInput for an editable tag field
1525
1883
 
1884
+ ## Item
1885
+ Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.
1886
+ Import: \`import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from "@usevyre/react"\`
1887
+
1888
+ Valid props:
1889
+ - variant: "default" | "outlined" | "muted" | "plain" [default: default]
1890
+ - size: "sm" | "md" | "lg" [default: md]
1891
+
1892
+ Never do:
1893
+ - ❌ Card used for repeated list rows → ✅ Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
1894
+ - ❌ Item variant="primary" → ✅ Use variant="default" | "outlined" | "muted"
1895
+ - ❌ raw text directly inside Item → ✅ Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
1896
+
1897
+ ## Kanban
1898
+ Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant="outlined"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.
1899
+ Import: \`import { Kanban } from "@usevyre/react"\`
1900
+
1901
+ Valid props:
1902
+
1903
+ Never do:
1904
+ - ❌ Kanban without onChange (or ignoring it) → ✅ Store columns in state and setColumns in onChange (v-model in Vue)
1905
+ - ❌ Duplicate card ids across columns → ✅ Use globally-unique card ids across the entire board
1906
+ - ❌ Mutating value in place then calling onChange → ✅ Pass the new array Kanban gives you straight to setState / v-model
1907
+ - ❌ color="blue" (or any non-semantic value) → ✅ Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
1908
+
1909
+ ## Conversation
1910
+ Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own \`value\` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.
1911
+ Import: \`import { Conversation } from "@usevyre/react"\`
1912
+
1913
+ Valid props:
1914
+
1915
+ Never do:
1916
+ - ❌ Conversation without currentUserId → ✅ Always pass currentUserId matching one of the message authorId values
1917
+ - ❌ Expecting Conversation to store/append messages → ✅ Append to your own state in onSend (or @send) and pass it back via value
1918
+ - ❌ composer without onSend (React) / @send (Vue) → ✅ Provide onSend / @send to append the message to value
1919
+ - ❌ Treating onSend as (text) only when using allowAttachments → ✅ Handle onSend(text, files) — map files to message attachments and append
1920
+
1921
+ ## DateRangePicker
1922
+ Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.
1923
+ Import: \`import { DateRangePicker } from "@usevyre/react"\`
1924
+
1925
+ Valid props:
1926
+ - numberOfMonths: "1" | "2" [default: 2]
1927
+ - weekStartsOn: "0" | "1" [default: 1]
1928
+
1929
+ Never do:
1930
+ - ❌ value={[from, to]} → ✅ Use value={{ from, to }} and read range.from / range.to
1931
+ - ❌ DateRangePicker for a single date → ✅ Use <DatePicker /> for a single date
1932
+ - ❌ presets="true" (string) → ✅ Use the bare prop: presets (or presets={true})
1933
+
1526
1934
  ## Token Rules
1527
1935
 
1528
1936
  Use --vyre-color-semantic-* for all colors. Never use primitive tokens.
1529
1937
  Use --vyre-spacing-* for all spacing. Never use raw px in component code.
1530
1938
  Use --vyre-border-radius-* for border radius.`;
1531
1939
  export const claudeContext = `# useVyre Design System Context
1532
- # Version: 1.1.0
1940
+ # Version: 1.2.0
1533
1941
 
1534
1942
  You are working in a codebase that uses the useVyre design system.
1535
1943
  Follow the rules below strictly when writing any UI code.
@@ -1841,7 +2249,7 @@ import { Button } from "@usevyre/react"
1841
2249
 
1842
2250
  ### Calendar
1843
2251
 
1844
- Date picker calendar widget for selecting single dates or ranges.
2252
+ Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.
1845
2253
 
1846
2254
  \`\`\`tsx
1847
2255
  import { Calendar } from "@usevyre/react"
@@ -1850,6 +2258,7 @@ import { Calendar } from "@usevyre/react"
1850
2258
  // value = Date | null
1851
2259
  // onChange = function
1852
2260
  // disabled = boolean (default: false)
2261
+ // defaultMonth = Date
1853
2262
 
1854
2263
  // Examples:
1855
2264
  const [date, setDate] = useState(null);
@@ -1857,7 +2266,39 @@ const [date, setDate] = useState(null);
1857
2266
  \`\`\`
1858
2267
 
1859
2268
  **Common mistakes:**
1860
- - ❌ \`Using Calendar for time selection\` Combine with a separate time Input if time selection is needed
2269
+ - ❌ \`Calendar for an input field that opens a popover\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
2270
+ - ❌ \`value as tuple for mode="single"\` → Pass value matching mode; use mode="range" for [start,end]
2271
+
2272
+ ---
2273
+
2274
+ ### DatePicker
2275
+
2276
+ Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.
2277
+
2278
+ \`\`\`tsx
2279
+ import { DatePicker } from "@usevyre/react"
2280
+
2281
+ // Props:
2282
+ // value = Date | [Date, Date] | Date[] | null
2283
+ // onChange = function
2284
+ // mode = "single" | "range" | "multiple" (default: single)
2285
+ // placeholder = string (default: Pick a date)
2286
+ // showTime = boolean (default: false)
2287
+ // minDate = Date
2288
+ // maxDate = Date
2289
+ // disabled = function
2290
+ // weekStartsOn = "0" | "1" (default: 1)
2291
+ // inputClassName = string
2292
+
2293
+ // Examples:
2294
+ const [date, setDate] = useState(null);
2295
+ <DatePicker value={date} onChange={setDate} placeholder="Pick a date" />
2296
+ <DatePicker value={date} onChange={setDate} showTime />
2297
+ \`\`\`
2298
+
2299
+ **Common mistakes:**
2300
+ - ❌ \`DatePicker mode="range" for { from, to } object\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
2301
+ - ❌ \`DatePicker without value/onChange\` → Provide value and onChange (e.g. from useState)
1861
2302
 
1862
2303
  ---
1863
2304
 
@@ -1917,6 +2358,78 @@ import { Checkbox } from "@usevyre/react"
1917
2358
 
1918
2359
  ---
1919
2360
 
2361
+ ### RadioGroup
2362
+
2363
+ Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.
2364
+
2365
+ \`\`\`tsx
2366
+ import { RadioGroup, Radio } from "@usevyre/react"
2367
+
2368
+ // Props:
2369
+ // value = string
2370
+ // defaultValue = string
2371
+ // onChange = function
2372
+ // name = string
2373
+ // disabled = boolean (default: false)
2374
+ // size = "sm" | "md" (default: md)
2375
+ // orientation = "vertical" | "horizontal" (default: vertical)
2376
+ // options = { value: string; label?: string; description?: string; disabled?: boolean }[]
2377
+
2378
+ // Examples:
2379
+ <RadioGroup
2380
+ value={plan}
2381
+ onChange={setPlan}
2382
+ options={[
2383
+ { value: "free", label: "Free", description: "For hobby projects" },
2384
+ { value: "pro", label: "Pro", description: "For teams" },
2385
+ ]}
2386
+ />
2387
+ <RadioGroup value={plan} onChange={setPlan} orientation="horizontal">
2388
+ <Radio value="free" label="Free" />
2389
+ <Radio value="pro" label="Pro" />
2390
+ </RadioGroup>
2391
+ \`\`\`
2392
+
2393
+ **Common mistakes:**
2394
+ - ❌ \`<Radio> used outside a <RadioGroup>\` → Always wrap <Radio> in <RadioGroup>
2395
+ - ❌ \`RadioGroup without value/onChange (React) or v-model (Vue)\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
2396
+ - ❌ \`Using Checkbox for mutually-exclusive choices\` → Use RadioGroup + Radio (or options) for one-of-many
2397
+
2398
+ ---
2399
+
2400
+ ### RichTextEditor
2401
+
2402
+ Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.
2403
+
2404
+ \`\`\`tsx
2405
+ import { RichTextEditor } from "@usevyre/react"
2406
+
2407
+ // Props:
2408
+ // value = string
2409
+ // onChange = function
2410
+ // placeholder = string (default: Write something…)
2411
+ // disabled = boolean (default: false)
2412
+ // readOnly = boolean (default: false)
2413
+ // toolbar = RichTextTool[]
2414
+ // minHeight = string (default: 10rem)
2415
+
2416
+ // Examples:
2417
+ const [html, setHtml] = useState("<p>Hello <strong>world</strong></p>");
2418
+ <RichTextEditor value={html} onChange={setHtml} placeholder="Write…" />
2419
+ <RichTextEditor
2420
+ value={html}
2421
+ onChange={setHtml}
2422
+ toolbar={["bold", "italic", "link"]}
2423
+ />
2424
+ \`\`\`
2425
+
2426
+ **Common mistakes:**
2427
+ - ❌ \`RichTextEditor without value/onChange (React) or v-model (Vue)\` → Keep the HTML string in state and update it in onChange / v-model
2428
+ - ❌ \`Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
2429
+ - ❌ \`toolbar="bold" (string)\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
2430
+
2431
+ ---
2432
+
1920
2433
  ### Command
1921
2434
 
1922
2435
  Command palette / search dialog. Use for search-first navigation or quick actions.
@@ -1968,7 +2481,7 @@ import { DropdownMenu, DropdownItem, DropdownSeparator, DropdownCheckboxItem, Dr
1968
2481
 
1969
2482
  ### Field
1970
2483
 
1971
- Form field wrapper providing label, hint text, and validation state for Input or Textarea.
2484
+ Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.
1972
2485
 
1973
2486
  \`\`\`tsx
1974
2487
  import { Field, Input, Textarea } from "@usevyre/react"
@@ -1986,10 +2499,23 @@ import { Field, Input, Textarea } from "@usevyre/react"
1986
2499
  <Field label="Search">
1987
2500
  <Input leftElement={<SearchIcon />} placeholder="Search..." />
1988
2501
  </Field>
2502
+ <Field>
2503
+ <FieldLabel required htmlFor="email">Email</FieldLabel>
2504
+ <Input id="email" type="email" />
2505
+ <FieldDescription>We\u2019ll never share it.</FieldDescription>
2506
+ <FieldError>{errors.email}</FieldError>
2507
+ </Field>
2508
+
2509
+ // Two controls side by side
2510
+ <FieldGroup orientation="horizontal">
2511
+ <Field label="First name"><Input /></Field>
2512
+ <Field label="Last name"><Input /></Field>
2513
+ </FieldGroup>
1989
2514
  \`\`\`
1990
2515
 
1991
2516
  **Common mistakes:**
1992
2517
  - ❌ \`Applying state prop directly to Input\` → Wrap Input in <Field state="error"> to apply validation styling
2518
+ - ❌ \`Mixing props label/hint AND FieldLabel/FieldError for the same field\` → Pick one: either props-based (label/hint/state) OR composable parts
1993
2519
 
1994
2520
  ---
1995
2521
 
@@ -2001,6 +2527,7 @@ Text input field. Wrap in Field for labels and validation. Use leftElement/right
2001
2527
  import { Input } from "@usevyre/react"
2002
2528
 
2003
2529
  // Props:
2530
+ // modelValue = string | number
2004
2531
  // size = "sm" | "md" | "lg" (default: md)
2005
2532
  // leftElement = ReactNode
2006
2533
  // rightElement = ReactNode
@@ -2012,6 +2539,7 @@ import { Input } from "@usevyre/react"
2012
2539
  **Common mistakes:**
2013
2540
  - ❌ \`size="icon"\` → Use size="sm" | "md" | "lg"
2014
2541
  - ❌ \`type="search" for search UI\` → Import Command from @usevyre/react for search palettes
2542
+ - ❌ \`Vue: binding Input/Textarea value without v-model\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
2015
2543
 
2016
2544
  ---
2017
2545
 
@@ -2599,6 +3127,180 @@ import { TagGroup, Tag } from "@usevyre/react"
2599
3127
 
2600
3128
  ---
2601
3129
 
3130
+ ### Item
3131
+
3132
+ Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.
3133
+
3134
+ \`\`\`tsx
3135
+ import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from "@usevyre/react"
3136
+
3137
+ // Props:
3138
+ // variant = "default" | "outlined" | "muted" | "plain" (default: default)
3139
+ // size = "sm" | "md" | "lg" (default: md)
3140
+ // clickable = boolean (default: false)
3141
+
3142
+ // Examples:
3143
+ <Item>
3144
+ <ItemMedia><BellIcon /></ItemMedia>
3145
+ <ItemContent>
3146
+ <ItemTitle>Notifications</ItemTitle>
3147
+ <ItemDescription>Receive an email when someone mentions you.</ItemDescription>
3148
+ </ItemContent>
3149
+ <ItemActions>
3150
+ <Switch defaultChecked />
3151
+ </ItemActions>
3152
+ </Item>
3153
+ <ItemGroup separated>
3154
+ <Item clickable>
3155
+ <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>
3156
+ </Item>
3157
+ <Item clickable>
3158
+ <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>
3159
+ </Item>
3160
+ </ItemGroup>
3161
+ \`\`\`
3162
+
3163
+ **Common mistakes:**
3164
+ - ❌ \`Card used for repeated list rows\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
3165
+ - ❌ \`Item variant="primary"\` → Use variant="default" | "outlined" | "muted"
3166
+ - ❌ \`raw text directly inside Item\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
3167
+
3168
+ ---
3169
+
3170
+ ### Kanban
3171
+
3172
+ Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant="outlined"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.
3173
+
3174
+ \`\`\`tsx
3175
+ import { Kanban } from "@usevyre/react"
3176
+
3177
+ // Props:
3178
+ // value = KanbanColumn[]
3179
+ // onChange = function
3180
+ // renderCard = function
3181
+ // onCardClick = function
3182
+
3183
+ // Examples:
3184
+ const [columns, setColumns] = useState([
3185
+ { id: "todo", title: "To Do", cards: [{ id: "1", title: "Spec API" }] },
3186
+ { id: "doing", title: "In Progress", cards: [] },
3187
+ { id: "done", title: "Done", cards: [{ id: "2", title: "Kickoff" }] },
3188
+ ]);
3189
+ <Kanban value={columns} onChange={setColumns} />
3190
+ <Kanban
3191
+ value={columns}
3192
+ onChange={setColumns}
3193
+ onCardClick={(card) => openDetail(card.id)}
3194
+ renderCard={(card) => (
3195
+ <><strong>{card.title}</strong><Badge>{card.id}</Badge></>
3196
+ )}
3197
+ />
3198
+ const [cols, setCols] = useState([
3199
+ { id: "doing", title: "In Progress", color: "teal", cards: [
3200
+ { id: "t1", title: "OAuth", assignee: "AK", progress: 60, color: "warning" },
3201
+ ]},
3202
+ ]);
3203
+ <Kanban
3204
+ value={cols}
3205
+ onChange={setCols}
3206
+ renderCard={(card) => (
3207
+ <><strong>{card.title}</strong><Progress value={card.progress} /></>
3208
+ )}
3209
+ />
3210
+ \`\`\`
3211
+
3212
+ **Common mistakes:**
3213
+ - ❌ \`Kanban without onChange (or ignoring it)\` → Store columns in state and setColumns in onChange (v-model in Vue)
3214
+ - ❌ \`Duplicate card ids across columns\` → Use globally-unique card ids across the entire board
3215
+ - ❌ \`Mutating value in place then calling onChange\` → Pass the new array Kanban gives you straight to setState / v-model
3216
+ - ❌ \`color="blue" (or any non-semantic value)\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
3217
+
3218
+ ---
3219
+
3220
+ ### Conversation
3221
+
3222
+ Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own \`value\` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.
3223
+
3224
+ \`\`\`tsx
3225
+ import { Conversation } from "@usevyre/react"
3226
+
3227
+ // Props:
3228
+ // value = ConversationMessage[]
3229
+ // currentUserId = string
3230
+ // composer = boolean (default: false)
3231
+ // onSend = function
3232
+ // placeholder = string (default: Write a message…)
3233
+ // typing = boolean | string (default: false)
3234
+ // allowAttachments = boolean (default: false)
3235
+ // accept = string
3236
+ // renderMessage = function
3237
+ // renderComposer = function
3238
+
3239
+ // Examples:
3240
+ const [messages, setMessages] = useState([
3241
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Hey!" },
3242
+ { id: "2", authorId: "me", text: "Hi \ud83d\udc4b", status: "read" },
3243
+ ]);
3244
+ <Conversation
3245
+ value={messages}
3246
+ currentUserId="me"
3247
+ composer
3248
+ onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: "me", text: t }])}
3249
+ />
3250
+ <Conversation
3251
+ value={messages}
3252
+ currentUserId="me"
3253
+ typing="Sam is typing"
3254
+ renderMessage={(m) => <strong>{m.text}</strong>}
3255
+ />
3256
+ const messages = [
3257
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Moodboard \ud83d\udc47",
3258
+ attachments: [{ kind: "image", url: "/board.png", name: "board.png" }] },
3259
+ { id: "2", authorId: "me", text: "Specs:", status: "read",
3260
+ attachments: [{ kind: "file", url: "/spec.pdf", name: "spec.pdf", size: "2.4 MB" }] },
3261
+ ];
3262
+ <Conversation value={messages} currentUserId="me" />
3263
+ \`\`\`
3264
+
3265
+ **Common mistakes:**
3266
+ - ❌ \`Conversation without currentUserId\` → Always pass currentUserId matching one of the message authorId values
3267
+ - ❌ \`Expecting Conversation to store/append messages\` → Append to your own state in onSend (or @send) and pass it back via value
3268
+ - ❌ \`composer without onSend (React) / @send (Vue)\` → Provide onSend / @send to append the message to value
3269
+ - ❌ \`Treating onSend as (text) only when using allowAttachments\` → Handle onSend(text, files) — map files to message attachments and append
3270
+
3271
+ ---
3272
+
3273
+ ### DateRangePicker
3274
+
3275
+ Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.
3276
+
3277
+ \`\`\`tsx
3278
+ import { DateRangePicker } from "@usevyre/react"
3279
+
3280
+ // Props:
3281
+ // value = { from: Date | null; to: Date | null } | null
3282
+ // onChange = function
3283
+ // placeholder = string (default: Pick a date range)
3284
+ // numberOfMonths = "1" | "2" (default: 2)
3285
+ // presets = boolean | DateRangePreset[] (default: false)
3286
+ // minDate = Date
3287
+ // maxDate = Date
3288
+ // disabled = function
3289
+ // weekStartsOn = "0" | "1" (default: 1)
3290
+
3291
+ // Examples:
3292
+ const [range, setRange] = useState({ from: null, to: null });
3293
+ <DateRangePicker value={range} onChange={setRange} presets />
3294
+ <DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />
3295
+ \`\`\`
3296
+
3297
+ **Common mistakes:**
3298
+ - ❌ \`value={[from, to]}\` → Use value={{ from, to }} and read range.from / range.to
3299
+ - ❌ \`DateRangePicker for a single date\` → Use <DatePicker /> for a single date
3300
+ - ❌ \`presets="true" (string)\` → Use the bare prop: presets (or presets={true})
3301
+
3302
+ ---
3303
+
2602
3304
  ## Hallucination Guard — Common AI Mistakes
2603
3305
 
2604
3306
  The following prop values and patterns do NOT exist in useVyre.
@@ -2618,14 +3320,25 @@ If you generate these, you are hallucinating.
2618
3320
  - ❌ \`<Button color="...">\` → Use variant prop instead
2619
3321
  - ❌ \`<Button icon={...}>\` → Use leftIcon={...} or rightIcon={...}
2620
3322
  - ❌ \`<Button size="icon" without aria-label>\` → Add aria-label describing the action
2621
- - ❌ \`<Calendar Using Calendar for time selection>\` Combine with a separate time Input if time selection is needed
3323
+ - ❌ \`<Calendar Calendar for an input field that opens a popover>\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
3324
+ - ❌ \`<Calendar value as tuple for mode="single">\` → Pass value matching mode; use mode="range" for [start,end]
3325
+ - ❌ \`<DatePicker DatePicker mode="range" for { from, to } object>\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
3326
+ - ❌ \`<DatePicker DatePicker without value/onChange>\` → Provide value and onChange (e.g. from useState)
2622
3327
  - ❌ \`<Card variant="primary">\` → Use variant="elevated" | "outlined" | "ghost" | "accent"
2623
3328
  - ❌ \`<Checkbox size="lg">\` → Use size="md"
3329
+ - ❌ \`<RadioGroup <Radio> used outside a <RadioGroup>>\` → Always wrap <Radio> in <RadioGroup>
3330
+ - ❌ \`<RadioGroup RadioGroup without value/onChange (React) or v-model (Vue)>\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
3331
+ - ❌ \`<RadioGroup Using Checkbox for mutually-exclusive choices>\` → Use RadioGroup + Radio (or options) for one-of-many
3332
+ - ❌ \`<RichTextEditor RichTextEditor without value/onChange (React) or v-model (Vue)>\` → Keep the HTML string in state and update it in onChange / v-model
3333
+ - ❌ \`<RichTextEditor Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising>\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
3334
+ - ❌ \`<RichTextEditor toolbar="bold" (string)>\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
2624
3335
  - ❌ \`<Command Using Input type="search" for search UI>\` → Use Command + CommandInput + CommandList + CommandItem
2625
3336
  - ❌ \`<DropdownMenu DropdownItem variant="primary">\` → Use variant="danger" for destructive items only
2626
3337
  - ❌ \`<Field Applying state prop directly to Input>\` → Wrap Input in <Field state="error"> to apply validation styling
3338
+ - ❌ \`<Field Mixing props label/hint AND FieldLabel/FieldError for the same field>\` → Pick one: either props-based (label/hint/state) OR composable parts
2627
3339
  - ❌ \`<Input size="icon">\` → Use size="sm" | "md" | "lg"
2628
3340
  - ❌ \`<Input type="search" for search UI>\` → Import Command from @usevyre/react for search palettes
3341
+ - ❌ \`<Input Vue: binding Input/Textarea value without v-model>\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
2629
3342
  - ❌ \`<Modal size="xl">\` → Use size="lg" or size="full"
2630
3343
  - ❌ \`<Popover placement="top-center">\` → Use placement="top" for centered placement
2631
3344
  - ❌ \`<Progress value > 100>\` → Normalize your value to 0–100 range before passing
@@ -2650,6 +3363,20 @@ If you generate these, you are hallucinating.
2650
3363
  - ❌ \`<Tag Tag size="xl">\` → Use size="lg"
2651
3364
  - ❌ \`<TagGroup TagGroup without Tag children>\` → Place <Tag> elements as direct children
2652
3365
  - ❌ \`<TagGroup Using TagGroup for tag input>\` → Use TagsInput for an editable tag field
3366
+ - ❌ \`<Item Card used for repeated list rows>\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
3367
+ - ❌ \`<Item Item variant="primary">\` → Use variant="default" | "outlined" | "muted"
3368
+ - ❌ \`<Item raw text directly inside Item>\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
3369
+ - ❌ \`<Kanban Kanban without onChange (or ignoring it)>\` → Store columns in state and setColumns in onChange (v-model in Vue)
3370
+ - ❌ \`<Kanban Duplicate card ids across columns>\` → Use globally-unique card ids across the entire board
3371
+ - ❌ \`<Kanban Mutating value in place then calling onChange>\` → Pass the new array Kanban gives you straight to setState / v-model
3372
+ - ❌ \`<Kanban color="blue" (or any non-semantic value)>\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
3373
+ - ❌ \`<Conversation Conversation without currentUserId>\` → Always pass currentUserId matching one of the message authorId values
3374
+ - ❌ \`<Conversation Expecting Conversation to store/append messages>\` → Append to your own state in onSend (or @send) and pass it back via value
3375
+ - ❌ \`<Conversation composer without onSend (React) / @send (Vue)>\` → Provide onSend / @send to append the message to value
3376
+ - ❌ \`<Conversation Treating onSend as (text) only when using allowAttachments>\` → Handle onSend(text, files) — map files to message attachments and append
3377
+ - ❌ \`<DateRangePicker value={[from, to]}>\` → Use value={{ from, to }} and read range.from / range.to
3378
+ - ❌ \`<DateRangePicker DateRangePicker for a single date>\` → Use <DatePicker /> for a single date
3379
+ - ❌ \`<DateRangePicker presets="true" (string)>\` → Use the bare prop: presets (or presets={true})
2653
3380
 
2654
3381
  ---
2655
3382
 
@@ -2699,7 +3426,7 @@ If you generate these, you are hallucinating.
2699
3426
  \`\`\`
2700
3427
  `;
2701
3428
  export const windsurfRules = `# useVyre Rules for Windsurf
2702
- # Version: 1.1.0
3429
+ # Version: 1.2.0
2703
3430
 
2704
3431
  # useVyre Design System — AI Context
2705
3432
  # Version: 0.2.0
@@ -3008,7 +3735,7 @@ import { Button } from "@usevyre/react"
3008
3735
 
3009
3736
  ### Calendar
3010
3737
 
3011
- Date picker calendar widget for selecting single dates or ranges.
3738
+ Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.
3012
3739
 
3013
3740
  \`\`\`tsx
3014
3741
  import { Calendar } from "@usevyre/react"
@@ -3017,6 +3744,7 @@ import { Calendar } from "@usevyre/react"
3017
3744
  // value = Date | null
3018
3745
  // onChange = function
3019
3746
  // disabled = boolean (default: false)
3747
+ // defaultMonth = Date
3020
3748
 
3021
3749
  // Examples:
3022
3750
  const [date, setDate] = useState(null);
@@ -3024,16 +3752,48 @@ const [date, setDate] = useState(null);
3024
3752
  \`\`\`
3025
3753
 
3026
3754
  **Common mistakes:**
3027
- - ❌ \`Using Calendar for time selection\` Combine with a separate time Input if time selection is needed
3755
+ - ❌ \`Calendar for an input field that opens a popover\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
3756
+ - ❌ \`value as tuple for mode="single"\` → Pass value matching mode; use mode="range" for [start,end]
3028
3757
 
3029
3758
  ---
3030
3759
 
3031
- ### Card
3760
+ ### DatePicker
3032
3761
 
3033
- Content container with optional header, body, and footer sections.
3762
+ Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.
3034
3763
 
3035
3764
  \`\`\`tsx
3036
- import { Card, CardHeader, CardBody, CardFooter } from "@usevyre/react"
3765
+ import { DatePicker } from "@usevyre/react"
3766
+
3767
+ // Props:
3768
+ // value = Date | [Date, Date] | Date[] | null
3769
+ // onChange = function
3770
+ // mode = "single" | "range" | "multiple" (default: single)
3771
+ // placeholder = string (default: Pick a date)
3772
+ // showTime = boolean (default: false)
3773
+ // minDate = Date
3774
+ // maxDate = Date
3775
+ // disabled = function
3776
+ // weekStartsOn = "0" | "1" (default: 1)
3777
+ // inputClassName = string
3778
+
3779
+ // Examples:
3780
+ const [date, setDate] = useState(null);
3781
+ <DatePicker value={date} onChange={setDate} placeholder="Pick a date" />
3782
+ <DatePicker value={date} onChange={setDate} showTime />
3783
+ \`\`\`
3784
+
3785
+ **Common mistakes:**
3786
+ - ❌ \`DatePicker mode="range" for { from, to } object\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
3787
+ - ❌ \`DatePicker without value/onChange\` → Provide value and onChange (e.g. from useState)
3788
+
3789
+ ---
3790
+
3791
+ ### Card
3792
+
3793
+ Content container with optional header, body, and footer sections.
3794
+
3795
+ \`\`\`tsx
3796
+ import { Card, CardHeader, CardBody, CardFooter } from "@usevyre/react"
3037
3797
 
3038
3798
  // Props:
3039
3799
  // variant = "default" | "elevated" | "outlined" | "ghost" | "accent" (default: default)
@@ -3084,6 +3844,78 @@ import { Checkbox } from "@usevyre/react"
3084
3844
 
3085
3845
  ---
3086
3846
 
3847
+ ### RadioGroup
3848
+
3849
+ Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.
3850
+
3851
+ \`\`\`tsx
3852
+ import { RadioGroup, Radio } from "@usevyre/react"
3853
+
3854
+ // Props:
3855
+ // value = string
3856
+ // defaultValue = string
3857
+ // onChange = function
3858
+ // name = string
3859
+ // disabled = boolean (default: false)
3860
+ // size = "sm" | "md" (default: md)
3861
+ // orientation = "vertical" | "horizontal" (default: vertical)
3862
+ // options = { value: string; label?: string; description?: string; disabled?: boolean }[]
3863
+
3864
+ // Examples:
3865
+ <RadioGroup
3866
+ value={plan}
3867
+ onChange={setPlan}
3868
+ options={[
3869
+ { value: "free", label: "Free", description: "For hobby projects" },
3870
+ { value: "pro", label: "Pro", description: "For teams" },
3871
+ ]}
3872
+ />
3873
+ <RadioGroup value={plan} onChange={setPlan} orientation="horizontal">
3874
+ <Radio value="free" label="Free" />
3875
+ <Radio value="pro" label="Pro" />
3876
+ </RadioGroup>
3877
+ \`\`\`
3878
+
3879
+ **Common mistakes:**
3880
+ - ❌ \`<Radio> used outside a <RadioGroup>\` → Always wrap <Radio> in <RadioGroup>
3881
+ - ❌ \`RadioGroup without value/onChange (React) or v-model (Vue)\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
3882
+ - ❌ \`Using Checkbox for mutually-exclusive choices\` → Use RadioGroup + Radio (or options) for one-of-many
3883
+
3884
+ ---
3885
+
3886
+ ### RichTextEditor
3887
+
3888
+ Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.
3889
+
3890
+ \`\`\`tsx
3891
+ import { RichTextEditor } from "@usevyre/react"
3892
+
3893
+ // Props:
3894
+ // value = string
3895
+ // onChange = function
3896
+ // placeholder = string (default: Write something…)
3897
+ // disabled = boolean (default: false)
3898
+ // readOnly = boolean (default: false)
3899
+ // toolbar = RichTextTool[]
3900
+ // minHeight = string (default: 10rem)
3901
+
3902
+ // Examples:
3903
+ const [html, setHtml] = useState("<p>Hello <strong>world</strong></p>");
3904
+ <RichTextEditor value={html} onChange={setHtml} placeholder="Write…" />
3905
+ <RichTextEditor
3906
+ value={html}
3907
+ onChange={setHtml}
3908
+ toolbar={["bold", "italic", "link"]}
3909
+ />
3910
+ \`\`\`
3911
+
3912
+ **Common mistakes:**
3913
+ - ❌ \`RichTextEditor without value/onChange (React) or v-model (Vue)\` → Keep the HTML string in state and update it in onChange / v-model
3914
+ - ❌ \`Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
3915
+ - ❌ \`toolbar="bold" (string)\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
3916
+
3917
+ ---
3918
+
3087
3919
  ### Command
3088
3920
 
3089
3921
  Command palette / search dialog. Use for search-first navigation or quick actions.
@@ -3135,7 +3967,7 @@ import { DropdownMenu, DropdownItem, DropdownSeparator, DropdownCheckboxItem, Dr
3135
3967
 
3136
3968
  ### Field
3137
3969
 
3138
- Form field wrapper providing label, hint text, and validation state for Input or Textarea.
3970
+ Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.
3139
3971
 
3140
3972
  \`\`\`tsx
3141
3973
  import { Field, Input, Textarea } from "@usevyre/react"
@@ -3153,10 +3985,23 @@ import { Field, Input, Textarea } from "@usevyre/react"
3153
3985
  <Field label="Search">
3154
3986
  <Input leftElement={<SearchIcon />} placeholder="Search..." />
3155
3987
  </Field>
3988
+ <Field>
3989
+ <FieldLabel required htmlFor="email">Email</FieldLabel>
3990
+ <Input id="email" type="email" />
3991
+ <FieldDescription>We\u2019ll never share it.</FieldDescription>
3992
+ <FieldError>{errors.email}</FieldError>
3993
+ </Field>
3994
+
3995
+ // Two controls side by side
3996
+ <FieldGroup orientation="horizontal">
3997
+ <Field label="First name"><Input /></Field>
3998
+ <Field label="Last name"><Input /></Field>
3999
+ </FieldGroup>
3156
4000
  \`\`\`
3157
4001
 
3158
4002
  **Common mistakes:**
3159
4003
  - ❌ \`Applying state prop directly to Input\` → Wrap Input in <Field state="error"> to apply validation styling
4004
+ - ❌ \`Mixing props label/hint AND FieldLabel/FieldError for the same field\` → Pick one: either props-based (label/hint/state) OR composable parts
3160
4005
 
3161
4006
  ---
3162
4007
 
@@ -3168,6 +4013,7 @@ Text input field. Wrap in Field for labels and validation. Use leftElement/right
3168
4013
  import { Input } from "@usevyre/react"
3169
4014
 
3170
4015
  // Props:
4016
+ // modelValue = string | number
3171
4017
  // size = "sm" | "md" | "lg" (default: md)
3172
4018
  // leftElement = ReactNode
3173
4019
  // rightElement = ReactNode
@@ -3179,6 +4025,7 @@ import { Input } from "@usevyre/react"
3179
4025
  **Common mistakes:**
3180
4026
  - ❌ \`size="icon"\` → Use size="sm" | "md" | "lg"
3181
4027
  - ❌ \`type="search" for search UI\` → Import Command from @usevyre/react for search palettes
4028
+ - ❌ \`Vue: binding Input/Textarea value without v-model\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
3182
4029
 
3183
4030
  ---
3184
4031
 
@@ -3766,6 +4613,180 @@ import { TagGroup, Tag } from "@usevyre/react"
3766
4613
 
3767
4614
  ---
3768
4615
 
4616
+ ### Item
4617
+
4618
+ Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.
4619
+
4620
+ \`\`\`tsx
4621
+ import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from "@usevyre/react"
4622
+
4623
+ // Props:
4624
+ // variant = "default" | "outlined" | "muted" | "plain" (default: default)
4625
+ // size = "sm" | "md" | "lg" (default: md)
4626
+ // clickable = boolean (default: false)
4627
+
4628
+ // Examples:
4629
+ <Item>
4630
+ <ItemMedia><BellIcon /></ItemMedia>
4631
+ <ItemContent>
4632
+ <ItemTitle>Notifications</ItemTitle>
4633
+ <ItemDescription>Receive an email when someone mentions you.</ItemDescription>
4634
+ </ItemContent>
4635
+ <ItemActions>
4636
+ <Switch defaultChecked />
4637
+ </ItemActions>
4638
+ </Item>
4639
+ <ItemGroup separated>
4640
+ <Item clickable>
4641
+ <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>
4642
+ </Item>
4643
+ <Item clickable>
4644
+ <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>
4645
+ </Item>
4646
+ </ItemGroup>
4647
+ \`\`\`
4648
+
4649
+ **Common mistakes:**
4650
+ - ❌ \`Card used for repeated list rows\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
4651
+ - ❌ \`Item variant="primary"\` → Use variant="default" | "outlined" | "muted"
4652
+ - ❌ \`raw text directly inside Item\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
4653
+
4654
+ ---
4655
+
4656
+ ### Kanban
4657
+
4658
+ Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant="outlined"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.
4659
+
4660
+ \`\`\`tsx
4661
+ import { Kanban } from "@usevyre/react"
4662
+
4663
+ // Props:
4664
+ // value = KanbanColumn[]
4665
+ // onChange = function
4666
+ // renderCard = function
4667
+ // onCardClick = function
4668
+
4669
+ // Examples:
4670
+ const [columns, setColumns] = useState([
4671
+ { id: "todo", title: "To Do", cards: [{ id: "1", title: "Spec API" }] },
4672
+ { id: "doing", title: "In Progress", cards: [] },
4673
+ { id: "done", title: "Done", cards: [{ id: "2", title: "Kickoff" }] },
4674
+ ]);
4675
+ <Kanban value={columns} onChange={setColumns} />
4676
+ <Kanban
4677
+ value={columns}
4678
+ onChange={setColumns}
4679
+ onCardClick={(card) => openDetail(card.id)}
4680
+ renderCard={(card) => (
4681
+ <><strong>{card.title}</strong><Badge>{card.id}</Badge></>
4682
+ )}
4683
+ />
4684
+ const [cols, setCols] = useState([
4685
+ { id: "doing", title: "In Progress", color: "teal", cards: [
4686
+ { id: "t1", title: "OAuth", assignee: "AK", progress: 60, color: "warning" },
4687
+ ]},
4688
+ ]);
4689
+ <Kanban
4690
+ value={cols}
4691
+ onChange={setCols}
4692
+ renderCard={(card) => (
4693
+ <><strong>{card.title}</strong><Progress value={card.progress} /></>
4694
+ )}
4695
+ />
4696
+ \`\`\`
4697
+
4698
+ **Common mistakes:**
4699
+ - ❌ \`Kanban without onChange (or ignoring it)\` → Store columns in state and setColumns in onChange (v-model in Vue)
4700
+ - ❌ \`Duplicate card ids across columns\` → Use globally-unique card ids across the entire board
4701
+ - ❌ \`Mutating value in place then calling onChange\` → Pass the new array Kanban gives you straight to setState / v-model
4702
+ - ❌ \`color="blue" (or any non-semantic value)\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
4703
+
4704
+ ---
4705
+
4706
+ ### Conversation
4707
+
4708
+ Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own \`value\` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.
4709
+
4710
+ \`\`\`tsx
4711
+ import { Conversation } from "@usevyre/react"
4712
+
4713
+ // Props:
4714
+ // value = ConversationMessage[]
4715
+ // currentUserId = string
4716
+ // composer = boolean (default: false)
4717
+ // onSend = function
4718
+ // placeholder = string (default: Write a message…)
4719
+ // typing = boolean | string (default: false)
4720
+ // allowAttachments = boolean (default: false)
4721
+ // accept = string
4722
+ // renderMessage = function
4723
+ // renderComposer = function
4724
+
4725
+ // Examples:
4726
+ const [messages, setMessages] = useState([
4727
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Hey!" },
4728
+ { id: "2", authorId: "me", text: "Hi \ud83d\udc4b", status: "read" },
4729
+ ]);
4730
+ <Conversation
4731
+ value={messages}
4732
+ currentUserId="me"
4733
+ composer
4734
+ onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: "me", text: t }])}
4735
+ />
4736
+ <Conversation
4737
+ value={messages}
4738
+ currentUserId="me"
4739
+ typing="Sam is typing"
4740
+ renderMessage={(m) => <strong>{m.text}</strong>}
4741
+ />
4742
+ const messages = [
4743
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Moodboard \ud83d\udc47",
4744
+ attachments: [{ kind: "image", url: "/board.png", name: "board.png" }] },
4745
+ { id: "2", authorId: "me", text: "Specs:", status: "read",
4746
+ attachments: [{ kind: "file", url: "/spec.pdf", name: "spec.pdf", size: "2.4 MB" }] },
4747
+ ];
4748
+ <Conversation value={messages} currentUserId="me" />
4749
+ \`\`\`
4750
+
4751
+ **Common mistakes:**
4752
+ - ❌ \`Conversation without currentUserId\` → Always pass currentUserId matching one of the message authorId values
4753
+ - ❌ \`Expecting Conversation to store/append messages\` → Append to your own state in onSend (or @send) and pass it back via value
4754
+ - ❌ \`composer without onSend (React) / @send (Vue)\` → Provide onSend / @send to append the message to value
4755
+ - ❌ \`Treating onSend as (text) only when using allowAttachments\` → Handle onSend(text, files) — map files to message attachments and append
4756
+
4757
+ ---
4758
+
4759
+ ### DateRangePicker
4760
+
4761
+ Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.
4762
+
4763
+ \`\`\`tsx
4764
+ import { DateRangePicker } from "@usevyre/react"
4765
+
4766
+ // Props:
4767
+ // value = { from: Date | null; to: Date | null } | null
4768
+ // onChange = function
4769
+ // placeholder = string (default: Pick a date range)
4770
+ // numberOfMonths = "1" | "2" (default: 2)
4771
+ // presets = boolean | DateRangePreset[] (default: false)
4772
+ // minDate = Date
4773
+ // maxDate = Date
4774
+ // disabled = function
4775
+ // weekStartsOn = "0" | "1" (default: 1)
4776
+
4777
+ // Examples:
4778
+ const [range, setRange] = useState({ from: null, to: null });
4779
+ <DateRangePicker value={range} onChange={setRange} presets />
4780
+ <DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />
4781
+ \`\`\`
4782
+
4783
+ **Common mistakes:**
4784
+ - ❌ \`value={[from, to]}\` → Use value={{ from, to }} and read range.from / range.to
4785
+ - ❌ \`DateRangePicker for a single date\` → Use <DatePicker /> for a single date
4786
+ - ❌ \`presets="true" (string)\` → Use the bare prop: presets (or presets={true})
4787
+
4788
+ ---
4789
+
3769
4790
  ## Hallucination Guard — Common AI Mistakes
3770
4791
 
3771
4792
  The following prop values and patterns do NOT exist in useVyre.
@@ -3785,14 +4806,25 @@ If you generate these, you are hallucinating.
3785
4806
  - ❌ \`<Button color="...">\` → Use variant prop instead
3786
4807
  - ❌ \`<Button icon={...}>\` → Use leftIcon={...} or rightIcon={...}
3787
4808
  - ❌ \`<Button size="icon" without aria-label>\` → Add aria-label describing the action
3788
- - ❌ \`<Calendar Using Calendar for time selection>\` Combine with a separate time Input if time selection is needed
4809
+ - ❌ \`<Calendar Calendar for an input field that opens a popover>\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
4810
+ - ❌ \`<Calendar value as tuple for mode="single">\` → Pass value matching mode; use mode="range" for [start,end]
4811
+ - ❌ \`<DatePicker DatePicker mode="range" for { from, to } object>\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
4812
+ - ❌ \`<DatePicker DatePicker without value/onChange>\` → Provide value and onChange (e.g. from useState)
3789
4813
  - ❌ \`<Card variant="primary">\` → Use variant="elevated" | "outlined" | "ghost" | "accent"
3790
4814
  - ❌ \`<Checkbox size="lg">\` → Use size="md"
4815
+ - ❌ \`<RadioGroup <Radio> used outside a <RadioGroup>>\` → Always wrap <Radio> in <RadioGroup>
4816
+ - ❌ \`<RadioGroup RadioGroup without value/onChange (React) or v-model (Vue)>\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
4817
+ - ❌ \`<RadioGroup Using Checkbox for mutually-exclusive choices>\` → Use RadioGroup + Radio (or options) for one-of-many
4818
+ - ❌ \`<RichTextEditor RichTextEditor without value/onChange (React) or v-model (Vue)>\` → Keep the HTML string in state and update it in onChange / v-model
4819
+ - ❌ \`<RichTextEditor Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising>\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
4820
+ - ❌ \`<RichTextEditor toolbar="bold" (string)>\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
3791
4821
  - ❌ \`<Command Using Input type="search" for search UI>\` → Use Command + CommandInput + CommandList + CommandItem
3792
4822
  - ❌ \`<DropdownMenu DropdownItem variant="primary">\` → Use variant="danger" for destructive items only
3793
4823
  - ❌ \`<Field Applying state prop directly to Input>\` → Wrap Input in <Field state="error"> to apply validation styling
4824
+ - ❌ \`<Field Mixing props label/hint AND FieldLabel/FieldError for the same field>\` → Pick one: either props-based (label/hint/state) OR composable parts
3794
4825
  - ❌ \`<Input size="icon">\` → Use size="sm" | "md" | "lg"
3795
4826
  - ❌ \`<Input type="search" for search UI>\` → Import Command from @usevyre/react for search palettes
4827
+ - ❌ \`<Input Vue: binding Input/Textarea value without v-model>\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
3796
4828
  - ❌ \`<Modal size="xl">\` → Use size="lg" or size="full"
3797
4829
  - ❌ \`<Popover placement="top-center">\` → Use placement="top" for centered placement
3798
4830
  - ❌ \`<Progress value > 100>\` → Normalize your value to 0–100 range before passing
@@ -3817,6 +4849,20 @@ If you generate these, you are hallucinating.
3817
4849
  - ❌ \`<Tag Tag size="xl">\` → Use size="lg"
3818
4850
  - ❌ \`<TagGroup TagGroup without Tag children>\` → Place <Tag> elements as direct children
3819
4851
  - ❌ \`<TagGroup Using TagGroup for tag input>\` → Use TagsInput for an editable tag field
4852
+ - ❌ \`<Item Card used for repeated list rows>\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
4853
+ - ❌ \`<Item Item variant="primary">\` → Use variant="default" | "outlined" | "muted"
4854
+ - ❌ \`<Item raw text directly inside Item>\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
4855
+ - ❌ \`<Kanban Kanban without onChange (or ignoring it)>\` → Store columns in state and setColumns in onChange (v-model in Vue)
4856
+ - ❌ \`<Kanban Duplicate card ids across columns>\` → Use globally-unique card ids across the entire board
4857
+ - ❌ \`<Kanban Mutating value in place then calling onChange>\` → Pass the new array Kanban gives you straight to setState / v-model
4858
+ - ❌ \`<Kanban color="blue" (or any non-semantic value)>\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
4859
+ - ❌ \`<Conversation Conversation without currentUserId>\` → Always pass currentUserId matching one of the message authorId values
4860
+ - ❌ \`<Conversation Expecting Conversation to store/append messages>\` → Append to your own state in onSend (or @send) and pass it back via value
4861
+ - ❌ \`<Conversation composer without onSend (React) / @send (Vue)>\` → Provide onSend / @send to append the message to value
4862
+ - ❌ \`<Conversation Treating onSend as (text) only when using allowAttachments>\` → Handle onSend(text, files) — map files to message attachments and append
4863
+ - ❌ \`<DateRangePicker value={[from, to]}>\` → Use value={{ from, to }} and read range.from / range.to
4864
+ - ❌ \`<DateRangePicker DateRangePicker for a single date>\` → Use <DatePicker /> for a single date
4865
+ - ❌ \`<DateRangePicker presets="true" (string)>\` → Use the bare prop: presets (or presets={true})
3820
4866
 
3821
4867
  ---
3822
4868
 
@@ -3866,7 +4912,7 @@ If you generate these, you are hallucinating.
3866
4912
  \`\`\`
3867
4913
  `;
3868
4914
  export const copilotInstructions = `# useVyre Copilot Instructions
3869
- # Version: 1.1.0
4915
+ # Version: 1.2.0
3870
4916
 
3871
4917
  When generating UI code in this project, follow the useVyre design system rules below.
3872
4918
 
@@ -4177,7 +5223,7 @@ import { Button } from "@usevyre/react"
4177
5223
 
4178
5224
  ### Calendar
4179
5225
 
4180
- Date picker calendar widget for selecting single dates or ranges.
5226
+ Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.
4181
5227
 
4182
5228
  \`\`\`tsx
4183
5229
  import { Calendar } from "@usevyre/react"
@@ -4186,6 +5232,7 @@ import { Calendar } from "@usevyre/react"
4186
5232
  // value = Date | null
4187
5233
  // onChange = function
4188
5234
  // disabled = boolean (default: false)
5235
+ // defaultMonth = Date
4189
5236
 
4190
5237
  // Examples:
4191
5238
  const [date, setDate] = useState(null);
@@ -4193,7 +5240,39 @@ const [date, setDate] = useState(null);
4193
5240
  \`\`\`
4194
5241
 
4195
5242
  **Common mistakes:**
4196
- - ❌ \`Using Calendar for time selection\` Combine with a separate time Input if time selection is needed
5243
+ - ❌ \`Calendar for an input field that opens a popover\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
5244
+ - ❌ \`value as tuple for mode="single"\` → Pass value matching mode; use mode="range" for [start,end]
5245
+
5246
+ ---
5247
+
5248
+ ### DatePicker
5249
+
5250
+ Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.
5251
+
5252
+ \`\`\`tsx
5253
+ import { DatePicker } from "@usevyre/react"
5254
+
5255
+ // Props:
5256
+ // value = Date | [Date, Date] | Date[] | null
5257
+ // onChange = function
5258
+ // mode = "single" | "range" | "multiple" (default: single)
5259
+ // placeholder = string (default: Pick a date)
5260
+ // showTime = boolean (default: false)
5261
+ // minDate = Date
5262
+ // maxDate = Date
5263
+ // disabled = function
5264
+ // weekStartsOn = "0" | "1" (default: 1)
5265
+ // inputClassName = string
5266
+
5267
+ // Examples:
5268
+ const [date, setDate] = useState(null);
5269
+ <DatePicker value={date} onChange={setDate} placeholder="Pick a date" />
5270
+ <DatePicker value={date} onChange={setDate} showTime />
5271
+ \`\`\`
5272
+
5273
+ **Common mistakes:**
5274
+ - ❌ \`DatePicker mode="range" for { from, to } object\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
5275
+ - ❌ \`DatePicker without value/onChange\` → Provide value and onChange (e.g. from useState)
4197
5276
 
4198
5277
  ---
4199
5278
 
@@ -4253,6 +5332,78 @@ import { Checkbox } from "@usevyre/react"
4253
5332
 
4254
5333
  ---
4255
5334
 
5335
+ ### RadioGroup
5336
+
5337
+ Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.
5338
+
5339
+ \`\`\`tsx
5340
+ import { RadioGroup, Radio } from "@usevyre/react"
5341
+
5342
+ // Props:
5343
+ // value = string
5344
+ // defaultValue = string
5345
+ // onChange = function
5346
+ // name = string
5347
+ // disabled = boolean (default: false)
5348
+ // size = "sm" | "md" (default: md)
5349
+ // orientation = "vertical" | "horizontal" (default: vertical)
5350
+ // options = { value: string; label?: string; description?: string; disabled?: boolean }[]
5351
+
5352
+ // Examples:
5353
+ <RadioGroup
5354
+ value={plan}
5355
+ onChange={setPlan}
5356
+ options={[
5357
+ { value: "free", label: "Free", description: "For hobby projects" },
5358
+ { value: "pro", label: "Pro", description: "For teams" },
5359
+ ]}
5360
+ />
5361
+ <RadioGroup value={plan} onChange={setPlan} orientation="horizontal">
5362
+ <Radio value="free" label="Free" />
5363
+ <Radio value="pro" label="Pro" />
5364
+ </RadioGroup>
5365
+ \`\`\`
5366
+
5367
+ **Common mistakes:**
5368
+ - ❌ \`<Radio> used outside a <RadioGroup>\` → Always wrap <Radio> in <RadioGroup>
5369
+ - ❌ \`RadioGroup without value/onChange (React) or v-model (Vue)\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
5370
+ - ❌ \`Using Checkbox for mutually-exclusive choices\` → Use RadioGroup + Radio (or options) for one-of-many
5371
+
5372
+ ---
5373
+
5374
+ ### RichTextEditor
5375
+
5376
+ Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.
5377
+
5378
+ \`\`\`tsx
5379
+ import { RichTextEditor } from "@usevyre/react"
5380
+
5381
+ // Props:
5382
+ // value = string
5383
+ // onChange = function
5384
+ // placeholder = string (default: Write something…)
5385
+ // disabled = boolean (default: false)
5386
+ // readOnly = boolean (default: false)
5387
+ // toolbar = RichTextTool[]
5388
+ // minHeight = string (default: 10rem)
5389
+
5390
+ // Examples:
5391
+ const [html, setHtml] = useState("<p>Hello <strong>world</strong></p>");
5392
+ <RichTextEditor value={html} onChange={setHtml} placeholder="Write…" />
5393
+ <RichTextEditor
5394
+ value={html}
5395
+ onChange={setHtml}
5396
+ toolbar={["bold", "italic", "link"]}
5397
+ />
5398
+ \`\`\`
5399
+
5400
+ **Common mistakes:**
5401
+ - ❌ \`RichTextEditor without value/onChange (React) or v-model (Vue)\` → Keep the HTML string in state and update it in onChange / v-model
5402
+ - ❌ \`Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
5403
+ - ❌ \`toolbar="bold" (string)\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
5404
+
5405
+ ---
5406
+
4256
5407
  ### Command
4257
5408
 
4258
5409
  Command palette / search dialog. Use for search-first navigation or quick actions.
@@ -4304,7 +5455,7 @@ import { DropdownMenu, DropdownItem, DropdownSeparator, DropdownCheckboxItem, Dr
4304
5455
 
4305
5456
  ### Field
4306
5457
 
4307
- Form field wrapper providing label, hint text, and validation state for Input or Textarea.
5458
+ Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.
4308
5459
 
4309
5460
  \`\`\`tsx
4310
5461
  import { Field, Input, Textarea } from "@usevyre/react"
@@ -4322,10 +5473,23 @@ import { Field, Input, Textarea } from "@usevyre/react"
4322
5473
  <Field label="Search">
4323
5474
  <Input leftElement={<SearchIcon />} placeholder="Search..." />
4324
5475
  </Field>
5476
+ <Field>
5477
+ <FieldLabel required htmlFor="email">Email</FieldLabel>
5478
+ <Input id="email" type="email" />
5479
+ <FieldDescription>We\u2019ll never share it.</FieldDescription>
5480
+ <FieldError>{errors.email}</FieldError>
5481
+ </Field>
5482
+
5483
+ // Two controls side by side
5484
+ <FieldGroup orientation="horizontal">
5485
+ <Field label="First name"><Input /></Field>
5486
+ <Field label="Last name"><Input /></Field>
5487
+ </FieldGroup>
4325
5488
  \`\`\`
4326
5489
 
4327
5490
  **Common mistakes:**
4328
5491
  - ❌ \`Applying state prop directly to Input\` → Wrap Input in <Field state="error"> to apply validation styling
5492
+ - ❌ \`Mixing props label/hint AND FieldLabel/FieldError for the same field\` → Pick one: either props-based (label/hint/state) OR composable parts
4329
5493
 
4330
5494
  ---
4331
5495
 
@@ -4337,6 +5501,7 @@ Text input field. Wrap in Field for labels and validation. Use leftElement/right
4337
5501
  import { Input } from "@usevyre/react"
4338
5502
 
4339
5503
  // Props:
5504
+ // modelValue = string | number
4340
5505
  // size = "sm" | "md" | "lg" (default: md)
4341
5506
  // leftElement = ReactNode
4342
5507
  // rightElement = ReactNode
@@ -4348,6 +5513,7 @@ import { Input } from "@usevyre/react"
4348
5513
  **Common mistakes:**
4349
5514
  - ❌ \`size="icon"\` → Use size="sm" | "md" | "lg"
4350
5515
  - ❌ \`type="search" for search UI\` → Import Command from @usevyre/react for search palettes
5516
+ - ❌ \`Vue: binding Input/Textarea value without v-model\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
4351
5517
 
4352
5518
  ---
4353
5519
 
@@ -4935,6 +6101,180 @@ import { TagGroup, Tag } from "@usevyre/react"
4935
6101
 
4936
6102
  ---
4937
6103
 
6104
+ ### Item
6105
+
6106
+ Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.
6107
+
6108
+ \`\`\`tsx
6109
+ import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from "@usevyre/react"
6110
+
6111
+ // Props:
6112
+ // variant = "default" | "outlined" | "muted" | "plain" (default: default)
6113
+ // size = "sm" | "md" | "lg" (default: md)
6114
+ // clickable = boolean (default: false)
6115
+
6116
+ // Examples:
6117
+ <Item>
6118
+ <ItemMedia><BellIcon /></ItemMedia>
6119
+ <ItemContent>
6120
+ <ItemTitle>Notifications</ItemTitle>
6121
+ <ItemDescription>Receive an email when someone mentions you.</ItemDescription>
6122
+ </ItemContent>
6123
+ <ItemActions>
6124
+ <Switch defaultChecked />
6125
+ </ItemActions>
6126
+ </Item>
6127
+ <ItemGroup separated>
6128
+ <Item clickable>
6129
+ <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>
6130
+ </Item>
6131
+ <Item clickable>
6132
+ <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>
6133
+ </Item>
6134
+ </ItemGroup>
6135
+ \`\`\`
6136
+
6137
+ **Common mistakes:**
6138
+ - ❌ \`Card used for repeated list rows\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
6139
+ - ❌ \`Item variant="primary"\` → Use variant="default" | "outlined" | "muted"
6140
+ - ❌ \`raw text directly inside Item\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
6141
+
6142
+ ---
6143
+
6144
+ ### Kanban
6145
+
6146
+ Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant="outlined"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.
6147
+
6148
+ \`\`\`tsx
6149
+ import { Kanban } from "@usevyre/react"
6150
+
6151
+ // Props:
6152
+ // value = KanbanColumn[]
6153
+ // onChange = function
6154
+ // renderCard = function
6155
+ // onCardClick = function
6156
+
6157
+ // Examples:
6158
+ const [columns, setColumns] = useState([
6159
+ { id: "todo", title: "To Do", cards: [{ id: "1", title: "Spec API" }] },
6160
+ { id: "doing", title: "In Progress", cards: [] },
6161
+ { id: "done", title: "Done", cards: [{ id: "2", title: "Kickoff" }] },
6162
+ ]);
6163
+ <Kanban value={columns} onChange={setColumns} />
6164
+ <Kanban
6165
+ value={columns}
6166
+ onChange={setColumns}
6167
+ onCardClick={(card) => openDetail(card.id)}
6168
+ renderCard={(card) => (
6169
+ <><strong>{card.title}</strong><Badge>{card.id}</Badge></>
6170
+ )}
6171
+ />
6172
+ const [cols, setCols] = useState([
6173
+ { id: "doing", title: "In Progress", color: "teal", cards: [
6174
+ { id: "t1", title: "OAuth", assignee: "AK", progress: 60, color: "warning" },
6175
+ ]},
6176
+ ]);
6177
+ <Kanban
6178
+ value={cols}
6179
+ onChange={setCols}
6180
+ renderCard={(card) => (
6181
+ <><strong>{card.title}</strong><Progress value={card.progress} /></>
6182
+ )}
6183
+ />
6184
+ \`\`\`
6185
+
6186
+ **Common mistakes:**
6187
+ - ❌ \`Kanban without onChange (or ignoring it)\` → Store columns in state and setColumns in onChange (v-model in Vue)
6188
+ - ❌ \`Duplicate card ids across columns\` → Use globally-unique card ids across the entire board
6189
+ - ❌ \`Mutating value in place then calling onChange\` → Pass the new array Kanban gives you straight to setState / v-model
6190
+ - ❌ \`color="blue" (or any non-semantic value)\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
6191
+
6192
+ ---
6193
+
6194
+ ### Conversation
6195
+
6196
+ Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own \`value\` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.
6197
+
6198
+ \`\`\`tsx
6199
+ import { Conversation } from "@usevyre/react"
6200
+
6201
+ // Props:
6202
+ // value = ConversationMessage[]
6203
+ // currentUserId = string
6204
+ // composer = boolean (default: false)
6205
+ // onSend = function
6206
+ // placeholder = string (default: Write a message…)
6207
+ // typing = boolean | string (default: false)
6208
+ // allowAttachments = boolean (default: false)
6209
+ // accept = string
6210
+ // renderMessage = function
6211
+ // renderComposer = function
6212
+
6213
+ // Examples:
6214
+ const [messages, setMessages] = useState([
6215
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Hey!" },
6216
+ { id: "2", authorId: "me", text: "Hi \ud83d\udc4b", status: "read" },
6217
+ ]);
6218
+ <Conversation
6219
+ value={messages}
6220
+ currentUserId="me"
6221
+ composer
6222
+ onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: "me", text: t }])}
6223
+ />
6224
+ <Conversation
6225
+ value={messages}
6226
+ currentUserId="me"
6227
+ typing="Sam is typing"
6228
+ renderMessage={(m) => <strong>{m.text}</strong>}
6229
+ />
6230
+ const messages = [
6231
+ { id: "1", authorId: "sam", authorName: "Sam", text: "Moodboard \ud83d\udc47",
6232
+ attachments: [{ kind: "image", url: "/board.png", name: "board.png" }] },
6233
+ { id: "2", authorId: "me", text: "Specs:", status: "read",
6234
+ attachments: [{ kind: "file", url: "/spec.pdf", name: "spec.pdf", size: "2.4 MB" }] },
6235
+ ];
6236
+ <Conversation value={messages} currentUserId="me" />
6237
+ \`\`\`
6238
+
6239
+ **Common mistakes:**
6240
+ - ❌ \`Conversation without currentUserId\` → Always pass currentUserId matching one of the message authorId values
6241
+ - ❌ \`Expecting Conversation to store/append messages\` → Append to your own state in onSend (or @send) and pass it back via value
6242
+ - ❌ \`composer without onSend (React) / @send (Vue)\` → Provide onSend / @send to append the message to value
6243
+ - ❌ \`Treating onSend as (text) only when using allowAttachments\` → Handle onSend(text, files) — map files to message attachments and append
6244
+
6245
+ ---
6246
+
6247
+ ### DateRangePicker
6248
+
6249
+ Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.
6250
+
6251
+ \`\`\`tsx
6252
+ import { DateRangePicker } from "@usevyre/react"
6253
+
6254
+ // Props:
6255
+ // value = { from: Date | null; to: Date | null } | null
6256
+ // onChange = function
6257
+ // placeholder = string (default: Pick a date range)
6258
+ // numberOfMonths = "1" | "2" (default: 2)
6259
+ // presets = boolean | DateRangePreset[] (default: false)
6260
+ // minDate = Date
6261
+ // maxDate = Date
6262
+ // disabled = function
6263
+ // weekStartsOn = "0" | "1" (default: 1)
6264
+
6265
+ // Examples:
6266
+ const [range, setRange] = useState({ from: null, to: null });
6267
+ <DateRangePicker value={range} onChange={setRange} presets />
6268
+ <DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />
6269
+ \`\`\`
6270
+
6271
+ **Common mistakes:**
6272
+ - ❌ \`value={[from, to]}\` → Use value={{ from, to }} and read range.from / range.to
6273
+ - ❌ \`DateRangePicker for a single date\` → Use <DatePicker /> for a single date
6274
+ - ❌ \`presets="true" (string)\` → Use the bare prop: presets (or presets={true})
6275
+
6276
+ ---
6277
+
4938
6278
  ## Hallucination Guard — Common AI Mistakes
4939
6279
 
4940
6280
  The following prop values and patterns do NOT exist in useVyre.
@@ -4954,14 +6294,25 @@ If you generate these, you are hallucinating.
4954
6294
  - ❌ \`<Button color="...">\` → Use variant prop instead
4955
6295
  - ❌ \`<Button icon={...}>\` → Use leftIcon={...} or rightIcon={...}
4956
6296
  - ❌ \`<Button size="icon" without aria-label>\` → Add aria-label describing the action
4957
- - ❌ \`<Calendar Using Calendar for time selection>\` Combine with a separate time Input if time selection is needed
6297
+ - ❌ \`<Calendar Calendar for an input field that opens a popover>\` Use <DatePicker /> (single date) or <DateRangePicker /> (range)
6298
+ - ❌ \`<Calendar value as tuple for mode="single">\` → Pass value matching mode; use mode="range" for [start,end]
6299
+ - ❌ \`<DatePicker DatePicker mode="range" for { from, to } object>\` → Use <DateRangePicker /> for the { from, to } object API + presets + dual month
6300
+ - ❌ \`<DatePicker DatePicker without value/onChange>\` → Provide value and onChange (e.g. from useState)
4958
6301
  - ❌ \`<Card variant="primary">\` → Use variant="elevated" | "outlined" | "ghost" | "accent"
4959
6302
  - ❌ \`<Checkbox size="lg">\` → Use size="md"
6303
+ - ❌ \`<RadioGroup <Radio> used outside a <RadioGroup>>\` → Always wrap <Radio> in <RadioGroup>
6304
+ - ❌ \`<RadioGroup RadioGroup without value/onChange (React) or v-model (Vue)>\` → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React
6305
+ - ❌ \`<RadioGroup Using Checkbox for mutually-exclusive choices>\` → Use RadioGroup + Radio (or options) for one-of-many
6306
+ - ❌ \`<RichTextEditor RichTextEditor without value/onChange (React) or v-model (Vue)>\` → Keep the HTML string in state and update it in onChange / v-model
6307
+ - ❌ \`<RichTextEditor Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising>\` → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output
6308
+ - ❌ \`<RichTextEditor toolbar="bold" (string)>\` → Pass an array, e.g. toolbar={["bold","italic","link"]}
4960
6309
  - ❌ \`<Command Using Input type="search" for search UI>\` → Use Command + CommandInput + CommandList + CommandItem
4961
6310
  - ❌ \`<DropdownMenu DropdownItem variant="primary">\` → Use variant="danger" for destructive items only
4962
6311
  - ❌ \`<Field Applying state prop directly to Input>\` → Wrap Input in <Field state="error"> to apply validation styling
6312
+ - ❌ \`<Field Mixing props label/hint AND FieldLabel/FieldError for the same field>\` → Pick one: either props-based (label/hint/state) OR composable parts
4963
6313
  - ❌ \`<Input size="icon">\` → Use size="sm" | "md" | "lg"
4964
6314
  - ❌ \`<Input type="search" for search UI>\` → Import Command from @usevyre/react for search palettes
6315
+ - ❌ \`<Input Vue: binding Input/Textarea value without v-model>\` → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange
4965
6316
  - ❌ \`<Modal size="xl">\` → Use size="lg" or size="full"
4966
6317
  - ❌ \`<Popover placement="top-center">\` → Use placement="top" for centered placement
4967
6318
  - ❌ \`<Progress value > 100>\` → Normalize your value to 0–100 range before passing
@@ -4986,6 +6337,20 @@ If you generate these, you are hallucinating.
4986
6337
  - ❌ \`<Tag Tag size="xl">\` → Use size="lg"
4987
6338
  - ❌ \`<TagGroup TagGroup without Tag children>\` → Place <Tag> elements as direct children
4988
6339
  - ❌ \`<TagGroup Using TagGroup for tag input>\` → Use TagsInput for an editable tag field
6340
+ - ❌ \`<Item Card used for repeated list rows>\` → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows
6341
+ - ❌ \`<Item Item variant="primary">\` → Use variant="default" | "outlined" | "muted"
6342
+ - ❌ \`<Item raw text directly inside Item>\` → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>
6343
+ - ❌ \`<Kanban Kanban without onChange (or ignoring it)>\` → Store columns in state and setColumns in onChange (v-model in Vue)
6344
+ - ❌ \`<Kanban Duplicate card ids across columns>\` → Use globally-unique card ids across the entire board
6345
+ - ❌ \`<Kanban Mutating value in place then calling onChange>\` → Pass the new array Kanban gives you straight to setState / v-model
6346
+ - ❌ \`<Kanban color="blue" (or any non-semantic value)>\` → Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
6347
+ - ❌ \`<Conversation Conversation without currentUserId>\` → Always pass currentUserId matching one of the message authorId values
6348
+ - ❌ \`<Conversation Expecting Conversation to store/append messages>\` → Append to your own state in onSend (or @send) and pass it back via value
6349
+ - ❌ \`<Conversation composer without onSend (React) / @send (Vue)>\` → Provide onSend / @send to append the message to value
6350
+ - ❌ \`<Conversation Treating onSend as (text) only when using allowAttachments>\` → Handle onSend(text, files) — map files to message attachments and append
6351
+ - ❌ \`<DateRangePicker value={[from, to]}>\` → Use value={{ from, to }} and read range.from / range.to
6352
+ - ❌ \`<DateRangePicker DateRangePicker for a single date>\` → Use <DatePicker /> for a single date
6353
+ - ❌ \`<DateRangePicker presets="true" (string)>\` → Use the bare prop: presets (or presets={true})
4989
6354
 
4990
6355
  ---
4991
6356
 
@@ -5037,8 +6402,8 @@ If you generate these, you are hallucinating.
5037
6402
 
5038
6403
  export const schema = {
5039
6404
  "$schema": "http://json-schema.org/draft-07/schema#",
5040
- "version": "1.1.0",
5041
- "generatedAt": "2026-05-15",
6405
+ "version": "1.2.0",
6406
+ "generatedAt": "2026-05-16",
5042
6407
  "package": "@usevyre/react",
5043
6408
  "packageVersion": "1.1.0",
5044
6409
  "validFor": [
@@ -5046,6 +6411,11 @@ export const schema = {
5046
6411
  "@usevyre/vue@1.1.0+"
5047
6412
  ],
5048
6413
  "changelog": {
6414
+ "1.2.0": {
6415
+ "date": "2026-05-16",
6416
+ "breaking": false,
6417
+ "summary": "Added Item layout primitive, DateRangePicker (dual-month range picker with presets, built on Calendar) and Kanban (controlled drag-and-drop board); Calendar gained additive defaultMonth prop; DatePicker split into its own documented component/entry"
6418
+ },
5049
6419
  "1.1.0": {
5050
6420
  "date": "2026-05-15",
5051
6421
  "breaking": false,
@@ -5386,7 +6756,7 @@ export const schema = {
5386
6756
  ]
5387
6757
  },
5388
6758
  "Calendar": {
5389
- "description": "Date picker calendar widget for selecting single dates or ranges.",
6759
+ "description": "Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.",
5390
6760
  "import": "import { Calendar } from \"@usevyre/react\"",
5391
6761
  "props": {
5392
6762
  "value": {
@@ -5400,13 +6770,22 @@ export const schema = {
5400
6770
  "disabled": {
5401
6771
  "type": "boolean",
5402
6772
  "default": false
6773
+ },
6774
+ "defaultMonth": {
6775
+ "type": "Date",
6776
+ "description": "Month to display initially when value is empty (uncontrolled view). Used by DateRangePicker for the second month."
5403
6777
  }
5404
6778
  },
5405
6779
  "antiPatterns": [
5406
6780
  {
5407
- "pattern": "Using Calendar for time selection",
5408
- "reason": "Calendar handles date only, not time",
5409
- "fix": "Combine with a separate time Input if time selection is needed"
6781
+ "pattern": "Calendar for an input field that opens a popover",
6782
+ "reason": "Calendar renders an always-visible grid, not a trigger",
6783
+ "fix": "Use <DatePicker /> (single date) or <DateRangePicker /> (range)"
6784
+ },
6785
+ {
6786
+ "pattern": "value as tuple for mode=\"single\"",
6787
+ "reason": "value type must match mode: Date for single, [Date,Date] for range, Date[] for multiple",
6788
+ "fix": "Pass value matching mode; use mode=\"range\" for [start,end]"
5410
6789
  }
5411
6790
  ],
5412
6791
  "examples": [
@@ -5416,39 +6795,120 @@ export const schema = {
5416
6795
  }
5417
6796
  ]
5418
6797
  },
5419
- "Card": {
5420
- "description": "Content container with optional header, body, and footer sections.",
5421
- "import": "import { Card, CardHeader, CardBody, CardFooter } from \"@usevyre/react\"",
5422
- "subcomponents": [
5423
- "CardHeader",
5424
- "CardBody",
5425
- "CardFooter"
5426
- ],
6798
+ "DatePicker": {
6799
+ "description": "Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.",
6800
+ "import": "import { DatePicker } from \"@usevyre/react\"",
5427
6801
  "props": {
5428
- "variant": {
6802
+ "value": {
6803
+ "type": "Date | [Date, Date] | Date[] | null",
6804
+ "description": "Selected value (controlled). Type must match mode."
6805
+ },
6806
+ "onChange": {
6807
+ "type": "function",
6808
+ "description": "Callback when the selection changes; argument shape matches mode"
6809
+ },
6810
+ "mode": {
5429
6811
  "type": "enum",
5430
6812
  "values": [
5431
- "default",
5432
- "elevated",
5433
- "outlined",
5434
- "ghost",
5435
- "accent"
6813
+ "single",
6814
+ "range",
6815
+ "multiple"
5436
6816
  ],
5437
- "default": "default",
5438
- "description": "Visual style of the card"
6817
+ "default": "single",
6818
+ "description": "Selection mode"
5439
6819
  },
5440
- "hoverable": {
5441
- "type": "boolean",
5442
- "default": false,
5443
- "description": "Adds hover lift effect"
6820
+ "placeholder": {
6821
+ "type": "string",
6822
+ "default": "Pick a date",
6823
+ "description": "Trigger text when nothing is selected"
5444
6824
  },
5445
- "clickable": {
6825
+ "showTime": {
5446
6826
  "type": "boolean",
5447
6827
  "default": false,
5448
- "description": "Adds cursor pointer and active press feedback"
5449
- }
5450
- },
5451
- "antiPatterns": [
6828
+ "description": "Adds an HH:MM time picker (single mode)"
6829
+ },
6830
+ "minDate": {
6831
+ "type": "Date",
6832
+ "description": "Earliest selectable date"
6833
+ },
6834
+ "maxDate": {
6835
+ "type": "Date",
6836
+ "description": "Latest selectable date"
6837
+ },
6838
+ "disabled": {
6839
+ "type": "function",
6840
+ "description": "(date: Date) => boolean — disable specific dates"
6841
+ },
6842
+ "weekStartsOn": {
6843
+ "type": "enum",
6844
+ "values": [
6845
+ 0,
6846
+ 1
6847
+ ],
6848
+ "default": 1,
6849
+ "description": "0 = Sunday, 1 = Monday"
6850
+ },
6851
+ "inputClassName": {
6852
+ "type": "string",
6853
+ "description": "Class on the trigger button"
6854
+ }
6855
+ },
6856
+ "antiPatterns": [
6857
+ {
6858
+ "pattern": "DatePicker mode=\"range\" for { from, to } object",
6859
+ "reason": "DatePicker range mode emits a [Date, Date] tuple, not a { from, to } object",
6860
+ "fix": "Use <DateRangePicker /> for the { from, to } object API + presets + dual month"
6861
+ },
6862
+ {
6863
+ "pattern": "DatePicker without value/onChange",
6864
+ "reason": "DatePicker is controlled — it has no internal selected state",
6865
+ "fix": "Provide value and onChange (e.g. from useState)"
6866
+ }
6867
+ ],
6868
+ "examples": [
6869
+ {
6870
+ "description": "Single date field",
6871
+ "code": "const [date, setDate] = useState(null);\n<DatePicker value={date} onChange={setDate} placeholder=\"Pick a date\" />"
6872
+ },
6873
+ {
6874
+ "description": "Date + time",
6875
+ "code": "<DatePicker value={date} onChange={setDate} showTime />"
6876
+ }
6877
+ ]
6878
+ },
6879
+ "Card": {
6880
+ "description": "Content container with optional header, body, and footer sections.",
6881
+ "import": "import { Card, CardHeader, CardBody, CardFooter } from \"@usevyre/react\"",
6882
+ "subcomponents": [
6883
+ "CardHeader",
6884
+ "CardBody",
6885
+ "CardFooter"
6886
+ ],
6887
+ "props": {
6888
+ "variant": {
6889
+ "type": "enum",
6890
+ "values": [
6891
+ "default",
6892
+ "elevated",
6893
+ "outlined",
6894
+ "ghost",
6895
+ "accent"
6896
+ ],
6897
+ "default": "default",
6898
+ "description": "Visual style of the card"
6899
+ },
6900
+ "hoverable": {
6901
+ "type": "boolean",
6902
+ "default": false,
6903
+ "description": "Adds hover lift effect"
6904
+ },
6905
+ "clickable": {
6906
+ "type": "boolean",
6907
+ "default": false,
6908
+ "description": "Adds cursor pointer and active press feedback"
6909
+ }
6910
+ },
6911
+ "antiPatterns": [
5452
6912
  {
5453
6913
  "pattern": "variant=\"primary\"",
5454
6914
  "reason": "Card has no 'primary' variant",
@@ -5506,6 +6966,150 @@ export const schema = {
5506
6966
  }
5507
6967
  ]
5508
6968
  },
6969
+ "RadioGroup": {
6970
+ "description": "Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.",
6971
+ "import": "import { RadioGroup, Radio } from \"@usevyre/react\"",
6972
+ "subcomponents": [
6973
+ "Radio"
6974
+ ],
6975
+ "props": {
6976
+ "value": {
6977
+ "type": "string",
6978
+ "description": "Selected value (controlled). Vue: v-model."
6979
+ },
6980
+ "defaultValue": {
6981
+ "type": "string",
6982
+ "description": "Initial value (uncontrolled, React only)"
6983
+ },
6984
+ "onChange": {
6985
+ "type": "function",
6986
+ "description": "(value: string) => void. Vue: update:modelValue / v-model."
6987
+ },
6988
+ "name": {
6989
+ "type": "string",
6990
+ "description": "Radio input name; auto-generated if omitted"
6991
+ },
6992
+ "disabled": {
6993
+ "type": "boolean",
6994
+ "default": false,
6995
+ "description": "Disable the whole group"
6996
+ },
6997
+ "size": {
6998
+ "type": "enum",
6999
+ "values": [
7000
+ "sm",
7001
+ "md"
7002
+ ],
7003
+ "default": "md",
7004
+ "description": "Control size"
7005
+ },
7006
+ "orientation": {
7007
+ "type": "enum",
7008
+ "values": [
7009
+ "vertical",
7010
+ "horizontal"
7011
+ ],
7012
+ "default": "vertical",
7013
+ "description": "Layout direction"
7014
+ },
7015
+ "options": {
7016
+ "type": "{ value: string; label?: string; description?: string; disabled?: boolean }[]",
7017
+ "description": "Data-driven options. Omit to pass <Radio> children instead."
7018
+ }
7019
+ },
7020
+ "antiPatterns": [
7021
+ {
7022
+ "pattern": "<Radio> used outside a <RadioGroup>",
7023
+ "reason": "Radio reads selection + name from RadioGroup context; it throws without a provider",
7024
+ "fix": "Always wrap <Radio> in <RadioGroup>"
7025
+ },
7026
+ {
7027
+ "pattern": "RadioGroup without value/onChange (React) or v-model (Vue)",
7028
+ "reason": "RadioGroup is controlled; selection won't update",
7029
+ "fix": "Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React"
7030
+ },
7031
+ {
7032
+ "pattern": "Using Checkbox for mutually-exclusive choices",
7033
+ "reason": "Radio expresses single-choice semantics + keyboard model",
7034
+ "fix": "Use RadioGroup + Radio (or options) for one-of-many"
7035
+ }
7036
+ ],
7037
+ "examples": [
7038
+ {
7039
+ "description": "Data-driven",
7040
+ "code": "<RadioGroup\n value={plan}\n onChange={setPlan}\n options={[\n { value: \"free\", label: \"Free\", description: \"For hobby projects\" },\n { value: \"pro\", label: \"Pro\", description: \"For teams\" },\n ]}\n/>"
7041
+ },
7042
+ {
7043
+ "description": "Composable children",
7044
+ "code": "<RadioGroup value={plan} onChange={setPlan} orientation=\"horizontal\">\n <Radio value=\"free\" label=\"Free\" />\n <Radio value=\"pro\" label=\"Pro\" />\n</RadioGroup>"
7045
+ }
7046
+ ]
7047
+ },
7048
+ "RichTextEditor": {
7049
+ "description": "Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.",
7050
+ "import": "import { RichTextEditor } from \"@usevyre/react\"",
7051
+ "props": {
7052
+ "value": {
7053
+ "type": "string",
7054
+ "description": "HTML content (controlled). React: value + onChange. Vue: v-model."
7055
+ },
7056
+ "onChange": {
7057
+ "type": "function",
7058
+ "description": "(html: string) => void. Vue: update:modelValue / v-model."
7059
+ },
7060
+ "placeholder": {
7061
+ "type": "string",
7062
+ "default": "Write something…",
7063
+ "description": "Shown when the editor is empty"
7064
+ },
7065
+ "disabled": {
7066
+ "type": "boolean",
7067
+ "default": false,
7068
+ "description": "Not editable, dimmed; toolbar disabled"
7069
+ },
7070
+ "readOnly": {
7071
+ "type": "boolean",
7072
+ "default": false,
7073
+ "description": "Not editable; toolbar hidden entirely"
7074
+ },
7075
+ "toolbar": {
7076
+ "type": "RichTextTool[]",
7077
+ "description": "Which buttons to show. Default = all. Tools: \"bold\"|\"italic\"|\"underline\"|\"strike\"|\"h1\"|\"h2\"|\"h3\"|\"ul\"|\"ol\"|\"quote\"|\"code\"|\"link\"|\"clear\""
7078
+ },
7079
+ "minHeight": {
7080
+ "type": "string",
7081
+ "default": "10rem",
7082
+ "description": "CSS min-height of the editable area"
7083
+ }
7084
+ },
7085
+ "antiPatterns": [
7086
+ {
7087
+ "pattern": "RichTextEditor without value/onChange (React) or v-model (Vue)",
7088
+ "reason": "It is fully controlled; edits won't persist",
7089
+ "fix": "Keep the HTML string in state and update it in onChange / v-model"
7090
+ },
7091
+ {
7092
+ "pattern": "Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising",
7093
+ "reason": "value is raw HTML the user typed",
7094
+ "fix": "Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output"
7095
+ },
7096
+ {
7097
+ "pattern": "toolbar=\"bold\" (string)",
7098
+ "reason": "toolbar is an array of tool ids",
7099
+ "fix": "Pass an array, e.g. toolbar={[\"bold\",\"italic\",\"link\"]}"
7100
+ }
7101
+ ],
7102
+ "examples": [
7103
+ {
7104
+ "description": "Controlled editor",
7105
+ "code": "const [html, setHtml] = useState(\"<p>Hello <strong>world</strong></p>\");\n<RichTextEditor value={html} onChange={setHtml} placeholder=\"Write…\" />"
7106
+ },
7107
+ {
7108
+ "description": "Minimal toolbar",
7109
+ "code": "<RichTextEditor\n value={html}\n onChange={setHtml}\n toolbar={[\"bold\", \"italic\", \"link\"]}\n/>"
7110
+ }
7111
+ ]
7112
+ },
5509
7113
  "Command": {
5510
7114
  "description": "Command palette / search dialog. Use for search-first navigation or quick actions.",
5511
7115
  "import": "import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandDialog } from \"@usevyre/react\"",
@@ -5575,7 +7179,7 @@ export const schema = {
5575
7179
  ]
5576
7180
  },
5577
7181
  "Field": {
5578
- "description": "Form field wrapper providing label, hint text, and validation state for Input or Textarea.",
7182
+ "description": "Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.",
5579
7183
  "import": "import { Field, Input, Textarea } from \"@usevyre/react\"",
5580
7184
  "props": {
5581
7185
  "label": {
@@ -5608,6 +7212,11 @@ export const schema = {
5608
7212
  "pattern": "Applying state prop directly to Input",
5609
7213
  "reason": "state belongs on Field, not on the Input itself",
5610
7214
  "fix": "Wrap Input in <Field state=\"error\"> to apply validation styling"
7215
+ },
7216
+ {
7217
+ "pattern": "Mixing props label/hint AND FieldLabel/FieldError for the same field",
7218
+ "reason": "Duplicates the label / message",
7219
+ "fix": "Pick one: either props-based (label/hint/state) OR composable parts"
5611
7220
  }
5612
7221
  ],
5613
7222
  "examples": [
@@ -5618,13 +7227,28 @@ export const schema = {
5618
7227
  {
5619
7228
  "description": "Search field with left icon",
5620
7229
  "code": "<Field label=\"Search\">\n <Input leftElement={<SearchIcon />} placeholder=\"Search...\" />\n</Field>"
7230
+ },
7231
+ {
7232
+ "description": "Composable field with explicit parts",
7233
+ "code": "<Field>\n <FieldLabel required htmlFor=\"email\">Email</FieldLabel>\n <Input id=\"email\" type=\"email\" />\n <FieldDescription>We\\u2019ll never share it.</FieldDescription>\n <FieldError>{errors.email}</FieldError>\n</Field>\n\n// Two controls side by side\n<FieldGroup orientation=\"horizontal\">\n <Field label=\"First name\"><Input /></Field>\n <Field label=\"Last name\"><Input /></Field>\n</FieldGroup>"
5621
7234
  }
7235
+ ],
7236
+ "subcomponents": [
7237
+ "FieldLabel",
7238
+ "FieldDescription",
7239
+ "FieldError",
7240
+ "FieldGroup",
7241
+ "FieldSet"
5622
7242
  ]
5623
7243
  },
5624
7244
  "Input": {
5625
7245
  "description": "Text input field. Wrap in Field for labels and validation. Use leftElement/rightElement for icons.",
5626
7246
  "import": "import { Input } from \"@usevyre/react\"",
5627
7247
  "props": {
7248
+ "modelValue": {
7249
+ "type": "string | number",
7250
+ "description": "Vue only: enables v-model (two-way binding). React: use native value + onChange. Native input attrs (type, placeholder, disabled…) work in both."
7251
+ },
5628
7252
  "size": {
5629
7253
  "type": "enum",
5630
7254
  "values": [
@@ -5654,6 +7278,11 @@ export const schema = {
5654
7278
  "pattern": "type=\"search\" for search UI",
5655
7279
  "reason": "Use Command component for search-first interfaces",
5656
7280
  "fix": "Import Command from @usevyre/react for search palettes"
7281
+ },
7282
+ {
7283
+ "pattern": "Vue: binding Input/Textarea value without v-model",
7284
+ "reason": "Vue Input & Textarea support v-model (modelValue); manual :value alone won't update",
7285
+ "fix": "Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange"
5657
7286
  }
5658
7287
  ],
5659
7288
  "examples": [
@@ -6678,12 +8307,304 @@ export const schema = {
6678
8307
  "code": "<TagGroup gap=\"sm\">\n <Tag>React</Tag>\n <Tag>Vue</Tag>\n <Tag variant=\"accent\">TypeScript</Tag>\n</TagGroup>"
6679
8308
  }
6680
8309
  ]
8310
+ },
8311
+ "Item": {
8312
+ "description": "Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.",
8313
+ "import": "import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from \"@usevyre/react\"",
8314
+ "subcomponents": [
8315
+ "ItemGroup",
8316
+ "ItemMedia",
8317
+ "ItemContent",
8318
+ "ItemTitle",
8319
+ "ItemDescription",
8320
+ "ItemActions"
8321
+ ],
8322
+ "props": {
8323
+ "variant": {
8324
+ "type": "enum",
8325
+ "values": [
8326
+ "default",
8327
+ "outlined",
8328
+ "muted",
8329
+ "plain"
8330
+ ],
8331
+ "default": "default",
8332
+ "description": "Visual style of the row. plain = transparent + no border (use when composing Item inside another surface like Card or a Kanban card to avoid a double background)."
8333
+ },
8334
+ "size": {
8335
+ "type": "enum",
8336
+ "values": [
8337
+ "sm",
8338
+ "md",
8339
+ "lg"
8340
+ ],
8341
+ "default": "md",
8342
+ "description": "Vertical density / text size of the row"
8343
+ },
8344
+ "clickable": {
8345
+ "type": "boolean",
8346
+ "default": false,
8347
+ "description": "Adds pointer cursor, hover background, focus ring, and role=button"
8348
+ }
8349
+ },
8350
+ "antiPatterns": [
8351
+ {
8352
+ "pattern": "Card used for repeated list rows",
8353
+ "reason": "Card is a content container; Item is the dense list-row primitive",
8354
+ "fix": "Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows"
8355
+ },
8356
+ {
8357
+ "pattern": "Item variant=\"primary\"",
8358
+ "reason": "Item has no 'primary' variant",
8359
+ "fix": "Use variant=\"default\" | \"outlined\" | \"muted\""
8360
+ },
8361
+ {
8362
+ "pattern": "raw text directly inside Item",
8363
+ "reason": "Item lays out media/content/actions columns; bare text breaks alignment",
8364
+ "fix": "Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>"
8365
+ }
8366
+ ],
8367
+ "examples": [
8368
+ {
8369
+ "description": "Settings row with media, content and a trailing switch",
8370
+ "code": "<Item>\n <ItemMedia><BellIcon /></ItemMedia>\n <ItemContent>\n <ItemTitle>Notifications</ItemTitle>\n <ItemDescription>Receive an email when someone mentions you.</ItemDescription>\n </ItemContent>\n <ItemActions>\n <Switch defaultChecked />\n </ItemActions>\n</Item>"
8371
+ },
8372
+ {
8373
+ "description": "Grouped clickable list with dividers",
8374
+ "code": "<ItemGroup separated>\n <Item clickable>\n <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>\n </Item>\n <Item clickable>\n <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>\n </Item>\n</ItemGroup>"
8375
+ }
8376
+ ]
8377
+ },
8378
+ "Kanban": {
8379
+ "description": "Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant=\"outlined\"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.",
8380
+ "import": "import { Kanban } from \"@usevyre/react\"",
8381
+ "props": {
8382
+ "value": {
8383
+ "type": "KanbanColumn[]",
8384
+ "description": "Controlled board data. KanbanColumn = { id, title, cards: KanbanCard[], color? }; KanbanCard = { id, title, description?, color? }. Extra app fields may ride along on cards and be read in renderCard. color = \"default\"|\"accent\"|\"teal\"|\"success\"|\"warning\"|\"danger\" (token-based tint). Card ids must be unique across the whole board."
8385
+ },
8386
+ "onChange": {
8387
+ "type": "function",
8388
+ "description": "(next: KanbanColumn[]) => void — called with the next columns array after a drag move. Required; set it back into your state."
8389
+ },
8390
+ "renderCard": {
8391
+ "type": "function",
8392
+ "description": "(card, column) => ReactNode — custom card body, rendered inside the wrapping <Card>. Vue: #card scoped slot. Can return any components (Avatar, Badge, Progress, etc.); attach extra fields to the card object and read them here."
8393
+ },
8394
+ "onCardClick": {
8395
+ "type": "function",
8396
+ "description": "(card, column) => void — fired on click/Enter/Space (not after a drag). Vue: @card-click."
8397
+ }
8398
+ },
8399
+ "antiPatterns": [
8400
+ {
8401
+ "pattern": "Kanban without onChange (or ignoring it)",
8402
+ "reason": "Kanban holds no internal data state; dropped cards won't move unless you apply onChange",
8403
+ "fix": "Store columns in state and setColumns in onChange (v-model in Vue)"
8404
+ },
8405
+ {
8406
+ "pattern": "Duplicate card ids across columns",
8407
+ "reason": "Card moves are resolved by id; duplicates corrupt the move",
8408
+ "fix": "Use globally-unique card ids across the entire board"
8409
+ },
8410
+ {
8411
+ "pattern": "Mutating value in place then calling onChange",
8412
+ "reason": "React/Vue need a new array reference to re-render reliably",
8413
+ "fix": "Pass the new array Kanban gives you straight to setState / v-model"
8414
+ },
8415
+ {
8416
+ "pattern": "color=\"blue\" (or any non-semantic value)",
8417
+ "reason": "color is a fixed semantic set, not an arbitrary CSS color",
8418
+ "fix": "Use one of: \"default\" | \"accent\" | \"teal\" | \"success\" | \"warning\" | \"danger\""
8419
+ }
8420
+ ],
8421
+ "examples": [
8422
+ {
8423
+ "description": "Controlled board",
8424
+ "code": "const [columns, setColumns] = useState([\n { id: \"todo\", title: \"To Do\", cards: [{ id: \"1\", title: \"Spec API\" }] },\n { id: \"doing\", title: \"In Progress\", cards: [] },\n { id: \"done\", title: \"Done\", cards: [{ id: \"2\", title: \"Kickoff\" }] },\n]);\n<Kanban value={columns} onChange={setColumns} />"
8425
+ },
8426
+ {
8427
+ "description": "Custom card body + click handler",
8428
+ "code": "<Kanban\n value={columns}\n onChange={setColumns}\n onCardClick={(card) => openDetail(card.id)}\n renderCard={(card) => (\n <><strong>{card.title}</strong><Badge>{card.id}</Badge></>\n )}\n/>"
8429
+ },
8430
+ {
8431
+ "description": "Tinted columns/cards + complex card content",
8432
+ "code": "const [cols, setCols] = useState([\n { id: \"doing\", title: \"In Progress\", color: \"teal\", cards: [\n { id: \"t1\", title: \"OAuth\", assignee: \"AK\", progress: 60, color: \"warning\" },\n ]},\n]);\n<Kanban\n value={cols}\n onChange={setCols}\n renderCard={(card) => (\n <><strong>{card.title}</strong><Progress value={card.progress} /></>\n )}\n/>"
8433
+ }
8434
+ ]
8435
+ },
8436
+ "Conversation": {
8437
+ "description": "Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own `value` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.",
8438
+ "import": "import { Conversation } from \"@usevyre/react\"",
8439
+ "props": {
8440
+ "value": {
8441
+ "type": "ConversationMessage[]",
8442
+ "description": "Ordered messages. ConversationMessage = { id, authorId, text?, authorName?, authorAvatar?, timestamp?: Date|string|number, status?: \"sending\"|\"sent\"|\"delivered\"|\"read\", attachments?: ConversationAttachment[] }. ConversationAttachment = { kind: \"image\"|\"audio\"|\"video\"|\"file\", url, name?, size? } — rendered inside the bubble. Required & controlled."
8443
+ },
8444
+ "currentUserId": {
8445
+ "type": "string",
8446
+ "description": "Whose messages are outgoing (aligned right). Required — match against message.authorId."
8447
+ },
8448
+ "composer": {
8449
+ "type": "boolean",
8450
+ "default": false,
8451
+ "description": "Show the built-in input + Send button. Pair with onSend."
8452
+ },
8453
+ "onSend": {
8454
+ "type": "function",
8455
+ "description": "(text: string, files: File[]) => void — called when the built-in composer submits. files holds anything staged via the attach button (empty array unless allowAttachments). You own the upload + turning staged files into message attachments. Vue: @send."
8456
+ },
8457
+ "placeholder": {
8458
+ "type": "string",
8459
+ "default": "Write a message…",
8460
+ "description": "Composer input placeholder."
8461
+ },
8462
+ "typing": {
8463
+ "type": "boolean | string",
8464
+ "default": false,
8465
+ "description": "Show an incoming typing indicator. Pass a string to label it."
8466
+ },
8467
+ "allowAttachments": {
8468
+ "type": "boolean",
8469
+ "default": false,
8470
+ "description": "Show a 📎 attach button + staged-file chips in the built-in composer. Files come back via onSend's second arg."
8471
+ },
8472
+ "accept": {
8473
+ "type": "string",
8474
+ "description": "Forwarded to the file input's accept attribute, e.g. \"image/*\"."
8475
+ },
8476
+ "renderMessage": {
8477
+ "type": "function",
8478
+ "description": "(message, meta) => ReactNode — custom bubble body. meta = { outgoing, isGroupStart, isGroupEnd }. Vue: #message scoped slot."
8479
+ },
8480
+ "renderComposer": {
8481
+ "type": "function",
8482
+ "description": "(api) => ReactNode — replace the composer entirely. api = { value, setValue, files, setFiles, send }. Vue: #composer scoped slot."
8483
+ }
8484
+ },
8485
+ "antiPatterns": [
8486
+ {
8487
+ "pattern": "Conversation without currentUserId",
8488
+ "reason": "Alignment (incoming vs outgoing) is decided by authorId === currentUserId",
8489
+ "fix": "Always pass currentUserId matching one of the message authorId values"
8490
+ },
8491
+ {
8492
+ "pattern": "Expecting Conversation to store/append messages",
8493
+ "reason": "Conversation is controlled & stateless for data, like Kanban/DataGrid",
8494
+ "fix": "Append to your own state in onSend (or @send) and pass it back via value"
8495
+ },
8496
+ {
8497
+ "pattern": "composer without onSend (React) / @send (Vue)",
8498
+ "reason": "The built-in composer emits text; nothing happens unless you handle it",
8499
+ "fix": "Provide onSend / @send to append the message to value"
8500
+ },
8501
+ {
8502
+ "pattern": "Treating onSend as (text) only when using allowAttachments",
8503
+ "reason": "With allowAttachments, staged files arrive as the 2nd arg; ignoring it drops attachments",
8504
+ "fix": "Handle onSend(text, files) — map files to message attachments and append"
8505
+ }
8506
+ ],
8507
+ "examples": [
8508
+ {
8509
+ "description": "Controlled thread with built-in composer",
8510
+ "code": "const [messages, setMessages] = useState([\n { id: \"1\", authorId: \"sam\", authorName: \"Sam\", text: \"Hey!\" },\n { id: \"2\", authorId: \"me\", text: \"Hi \\ud83d\\udc4b\", status: \"read\" },\n]);\n<Conversation\n value={messages}\n currentUserId=\"me\"\n composer\n onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: \"me\", text: t }])}\n/>"
8511
+ },
8512
+ {
8513
+ "description": "Typing indicator + custom bubble",
8514
+ "code": "<Conversation\n value={messages}\n currentUserId=\"me\"\n typing=\"Sam is typing\"\n renderMessage={(m) => <strong>{m.text}</strong>}\n/>"
8515
+ },
8516
+ {
8517
+ "description": "Message with image + file attachments",
8518
+ "code": "const messages = [\n { id: \"1\", authorId: \"sam\", authorName: \"Sam\", text: \"Moodboard \\ud83d\\udc47\",\n attachments: [{ kind: \"image\", url: \"/board.png\", name: \"board.png\" }] },\n { id: \"2\", authorId: \"me\", text: \"Specs:\", status: \"read\",\n attachments: [{ kind: \"file\", url: \"/spec.pdf\", name: \"spec.pdf\", size: \"2.4 MB\" }] },\n];\n<Conversation value={messages} currentUserId=\"me\" />"
8519
+ }
8520
+ ]
8521
+ },
8522
+ "DateRangePicker": {
8523
+ "description": "Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.",
8524
+ "import": "import { DateRangePicker } from \"@usevyre/react\"",
8525
+ "props": {
8526
+ "value": {
8527
+ "type": "{ from: Date | null; to: Date | null } | null",
8528
+ "description": "Selected range (controlled). Pass an OBJECT, not a tuple."
8529
+ },
8530
+ "onChange": {
8531
+ "type": "function",
8532
+ "description": "Callback (range: { from, to }) => void when the range changes"
8533
+ },
8534
+ "placeholder": {
8535
+ "type": "string",
8536
+ "default": "Pick a date range",
8537
+ "description": "Trigger text when no range is selected"
8538
+ },
8539
+ "numberOfMonths": {
8540
+ "type": "enum",
8541
+ "values": [
8542
+ 1,
8543
+ 2
8544
+ ],
8545
+ "default": 2,
8546
+ "description": "How many month calendars to show side-by-side"
8547
+ },
8548
+ "presets": {
8549
+ "type": "boolean | DateRangePreset[]",
8550
+ "default": false,
8551
+ "description": "true shows built-in presets (Today, Yesterday, Last 7/30 days, This/Last month); or pass a custom array of { label, range() }"
8552
+ },
8553
+ "minDate": {
8554
+ "type": "Date",
8555
+ "description": "Earliest selectable date"
8556
+ },
8557
+ "maxDate": {
8558
+ "type": "Date",
8559
+ "description": "Latest selectable date"
8560
+ },
8561
+ "disabled": {
8562
+ "type": "function",
8563
+ "description": "(date: Date) => boolean — disable specific dates"
8564
+ },
8565
+ "weekStartsOn": {
8566
+ "type": "enum",
8567
+ "values": [
8568
+ 0,
8569
+ 1
8570
+ ],
8571
+ "default": 1,
8572
+ "description": "0 = Sunday, 1 = Monday"
8573
+ }
8574
+ },
8575
+ "antiPatterns": [
8576
+ {
8577
+ "pattern": "value={[from, to]}",
8578
+ "reason": "DateRangePicker uses a { from, to } object, not a [Date, Date] tuple like Calendar mode=range",
8579
+ "fix": "Use value={{ from, to }} and read range.from / range.to"
8580
+ },
8581
+ {
8582
+ "pattern": "DateRangePicker for a single date",
8583
+ "reason": "DateRangePicker always selects a start AND end",
8584
+ "fix": "Use <DatePicker /> for a single date"
8585
+ },
8586
+ {
8587
+ "pattern": "presets=\"true\" (string)",
8588
+ "reason": "presets is a boolean or an array, not a string",
8589
+ "fix": "Use the bare prop: presets (or presets={true})"
8590
+ }
8591
+ ],
8592
+ "examples": [
8593
+ {
8594
+ "description": "Range picker with built-in presets",
8595
+ "code": "const [range, setRange] = useState({ from: null, to: null });\n<DateRangePicker value={range} onChange={setRange} presets />"
8596
+ },
8597
+ {
8598
+ "description": "Single month, no presets",
8599
+ "code": "<DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />"
8600
+ }
8601
+ ]
6681
8602
  }
6682
8603
  }
6683
8604
  };
6684
8605
 
6685
8606
  export const antiPatterns = {
6686
- "version": "1.1.0",
8607
+ "version": "1.2.0",
6687
8608
  "rules": [
6688
8609
  {
6689
8610
  "component": "Accordion",
@@ -6785,9 +8706,30 @@ export const antiPatterns = {
6785
8706
  },
6786
8707
  {
6787
8708
  "component": "Calendar",
6788
- "pattern": "Using Calendar for time selection",
6789
- "reason": "Calendar handles date only, not time",
6790
- "fix": "Combine with a separate time Input if time selection is needed",
8709
+ "pattern": "Calendar for an input field that opens a popover",
8710
+ "reason": "Calendar renders an always-visible grid, not a trigger",
8711
+ "fix": "Use <DatePicker /> (single date) or <DateRangePicker /> (range)",
8712
+ "severity": "error"
8713
+ },
8714
+ {
8715
+ "component": "Calendar",
8716
+ "pattern": "value as tuple for mode=\"single\"",
8717
+ "reason": "value type must match mode: Date for single, [Date,Date] for range, Date[] for multiple",
8718
+ "fix": "Pass value matching mode; use mode=\"range\" for [start,end]",
8719
+ "severity": "error"
8720
+ },
8721
+ {
8722
+ "component": "DatePicker",
8723
+ "pattern": "DatePicker mode=\"range\" for { from, to } object",
8724
+ "reason": "DatePicker range mode emits a [Date, Date] tuple, not a { from, to } object",
8725
+ "fix": "Use <DateRangePicker /> for the { from, to } object API + presets + dual month",
8726
+ "severity": "error"
8727
+ },
8728
+ {
8729
+ "component": "DatePicker",
8730
+ "pattern": "DatePicker without value/onChange",
8731
+ "reason": "DatePicker is controlled — it has no internal selected state",
8732
+ "fix": "Provide value and onChange (e.g. from useState)",
6791
8733
  "severity": "error"
6792
8734
  },
6793
8735
  {
@@ -6804,6 +8746,48 @@ export const antiPatterns = {
6804
8746
  "fix": "Use size=\"md\"",
6805
8747
  "severity": "error"
6806
8748
  },
8749
+ {
8750
+ "component": "RadioGroup",
8751
+ "pattern": "<Radio> used outside a <RadioGroup>",
8752
+ "reason": "Radio reads selection + name from RadioGroup context; it throws without a provider",
8753
+ "fix": "Always wrap <Radio> in <RadioGroup>",
8754
+ "severity": "error"
8755
+ },
8756
+ {
8757
+ "component": "RadioGroup",
8758
+ "pattern": "RadioGroup without value/onChange (React) or v-model (Vue)",
8759
+ "reason": "RadioGroup is controlled; selection won't update",
8760
+ "fix": "Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React",
8761
+ "severity": "error"
8762
+ },
8763
+ {
8764
+ "component": "RadioGroup",
8765
+ "pattern": "Using Checkbox for mutually-exclusive choices",
8766
+ "reason": "Radio expresses single-choice semantics + keyboard model",
8767
+ "fix": "Use RadioGroup + Radio (or options) for one-of-many",
8768
+ "severity": "error"
8769
+ },
8770
+ {
8771
+ "component": "RichTextEditor",
8772
+ "pattern": "RichTextEditor without value/onChange (React) or v-model (Vue)",
8773
+ "reason": "It is fully controlled; edits won't persist",
8774
+ "fix": "Keep the HTML string in state and update it in onChange / v-model",
8775
+ "severity": "error"
8776
+ },
8777
+ {
8778
+ "component": "RichTextEditor",
8779
+ "pattern": "Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising",
8780
+ "reason": "value is raw HTML the user typed",
8781
+ "fix": "Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output",
8782
+ "severity": "error"
8783
+ },
8784
+ {
8785
+ "component": "RichTextEditor",
8786
+ "pattern": "toolbar=\"bold\" (string)",
8787
+ "reason": "toolbar is an array of tool ids",
8788
+ "fix": "Pass an array, e.g. toolbar={[\"bold\",\"italic\",\"link\"]}",
8789
+ "severity": "error"
8790
+ },
6807
8791
  {
6808
8792
  "component": "Command",
6809
8793
  "pattern": "Using Input type=\"search\" for search UI",
@@ -6825,6 +8809,13 @@ export const antiPatterns = {
6825
8809
  "fix": "Wrap Input in <Field state=\"error\"> to apply validation styling",
6826
8810
  "severity": "error"
6827
8811
  },
8812
+ {
8813
+ "component": "Field",
8814
+ "pattern": "Mixing props label/hint AND FieldLabel/FieldError for the same field",
8815
+ "reason": "Duplicates the label / message",
8816
+ "fix": "Pick one: either props-based (label/hint/state) OR composable parts",
8817
+ "severity": "error"
8818
+ },
6828
8819
  {
6829
8820
  "component": "Input",
6830
8821
  "pattern": "size=\"icon\"",
@@ -6839,6 +8830,13 @@ export const antiPatterns = {
6839
8830
  "fix": "Import Command from @usevyre/react for search palettes",
6840
8831
  "severity": "error"
6841
8832
  },
8833
+ {
8834
+ "component": "Input",
8835
+ "pattern": "Vue: binding Input/Textarea value without v-model",
8836
+ "reason": "Vue Input & Textarea support v-model (modelValue); manual :value alone won't update",
8837
+ "fix": "Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange",
8838
+ "severity": "error"
8839
+ },
6842
8840
  {
6843
8841
  "component": "Modal",
6844
8842
  "pattern": "size=\"xl\"",
@@ -7006,19 +9004,122 @@ export const antiPatterns = {
7006
9004
  "reason": "TagGroup is display-only",
7007
9005
  "fix": "Use TagsInput for an editable tag field",
7008
9006
  "severity": "error"
9007
+ },
9008
+ {
9009
+ "component": "Item",
9010
+ "pattern": "Card used for repeated list rows",
9011
+ "reason": "Card is a content container; Item is the dense list-row primitive",
9012
+ "fix": "Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows",
9013
+ "severity": "error"
9014
+ },
9015
+ {
9016
+ "component": "Item",
9017
+ "pattern": "Item variant=\"primary\"",
9018
+ "reason": "Item has no 'primary' variant",
9019
+ "fix": "Use variant=\"default\" | \"outlined\" | \"muted\"",
9020
+ "severity": "error"
9021
+ },
9022
+ {
9023
+ "component": "Item",
9024
+ "pattern": "raw text directly inside Item",
9025
+ "reason": "Item lays out media/content/actions columns; bare text breaks alignment",
9026
+ "fix": "Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>",
9027
+ "severity": "error"
9028
+ },
9029
+ {
9030
+ "component": "Kanban",
9031
+ "pattern": "Kanban without onChange (or ignoring it)",
9032
+ "reason": "Kanban holds no internal data state; dropped cards won't move unless you apply onChange",
9033
+ "fix": "Store columns in state and setColumns in onChange (v-model in Vue)",
9034
+ "severity": "error"
9035
+ },
9036
+ {
9037
+ "component": "Kanban",
9038
+ "pattern": "Duplicate card ids across columns",
9039
+ "reason": "Card moves are resolved by id; duplicates corrupt the move",
9040
+ "fix": "Use globally-unique card ids across the entire board",
9041
+ "severity": "error"
9042
+ },
9043
+ {
9044
+ "component": "Kanban",
9045
+ "pattern": "Mutating value in place then calling onChange",
9046
+ "reason": "React/Vue need a new array reference to re-render reliably",
9047
+ "fix": "Pass the new array Kanban gives you straight to setState / v-model",
9048
+ "severity": "error"
9049
+ },
9050
+ {
9051
+ "component": "Kanban",
9052
+ "pattern": "color=\"blue\" (or any non-semantic value)",
9053
+ "reason": "color is a fixed semantic set, not an arbitrary CSS color",
9054
+ "fix": "Use one of: \"default\" | \"accent\" | \"teal\" | \"success\" | \"warning\" | \"danger\"",
9055
+ "severity": "error"
9056
+ },
9057
+ {
9058
+ "component": "Conversation",
9059
+ "pattern": "Conversation without currentUserId",
9060
+ "reason": "Alignment (incoming vs outgoing) is decided by authorId === currentUserId",
9061
+ "fix": "Always pass currentUserId matching one of the message authorId values",
9062
+ "severity": "error"
9063
+ },
9064
+ {
9065
+ "component": "Conversation",
9066
+ "pattern": "Expecting Conversation to store/append messages",
9067
+ "reason": "Conversation is controlled & stateless for data, like Kanban/DataGrid",
9068
+ "fix": "Append to your own state in onSend (or @send) and pass it back via value",
9069
+ "severity": "error"
9070
+ },
9071
+ {
9072
+ "component": "Conversation",
9073
+ "pattern": "composer without onSend (React) / @send (Vue)",
9074
+ "reason": "The built-in composer emits text; nothing happens unless you handle it",
9075
+ "fix": "Provide onSend / @send to append the message to value",
9076
+ "severity": "error"
9077
+ },
9078
+ {
9079
+ "component": "Conversation",
9080
+ "pattern": "Treating onSend as (text) only when using allowAttachments",
9081
+ "reason": "With allowAttachments, staged files arrive as the 2nd arg; ignoring it drops attachments",
9082
+ "fix": "Handle onSend(text, files) — map files to message attachments and append",
9083
+ "severity": "error"
9084
+ },
9085
+ {
9086
+ "component": "DateRangePicker",
9087
+ "pattern": "value={[from, to]}",
9088
+ "reason": "DateRangePicker uses a { from, to } object, not a [Date, Date] tuple like Calendar mode=range",
9089
+ "fix": "Use value={{ from, to }} and read range.from / range.to",
9090
+ "severity": "error"
9091
+ },
9092
+ {
9093
+ "component": "DateRangePicker",
9094
+ "pattern": "DateRangePicker for a single date",
9095
+ "reason": "DateRangePicker always selects a start AND end",
9096
+ "fix": "Use <DatePicker /> for a single date",
9097
+ "severity": "error"
9098
+ },
9099
+ {
9100
+ "component": "DateRangePicker",
9101
+ "pattern": "presets=\"true\" (string)",
9102
+ "reason": "presets is a boolean or an array, not a string",
9103
+ "fix": "Use the bare prop: presets (or presets={true})",
9104
+ "severity": "error"
7009
9105
  }
7010
9106
  ]
7011
9107
  };
7012
9108
 
7013
9109
  export const versionInfo = {
7014
- "version": "1.1.0",
9110
+ "version": "1.2.0",
7015
9111
  "packageVersion": "1.1.0",
7016
- "generatedAt": "2026-05-15T15:01:34.101Z",
9112
+ "generatedAt": "2026-05-17T04:15:52.069Z",
7017
9113
  "validFor": [
7018
9114
  "@usevyre/react@1.1.0+",
7019
9115
  "@usevyre/vue@1.1.0+"
7020
9116
  ],
7021
9117
  "changelog": {
9118
+ "1.2.0": {
9119
+ "date": "2026-05-16",
9120
+ "breaking": false,
9121
+ "summary": "Added Item layout primitive, DateRangePicker (dual-month range picker with presets, built on Calendar) and Kanban (controlled drag-and-drop board); Calendar gained additive defaultMonth prop; DatePicker split into its own documented component/entry"
9122
+ },
7022
9123
  "1.1.0": {
7023
9124
  "date": "2026-05-15",
7024
9125
  "breaking": false,
@@ -7043,252 +9144,301 @@ export const versionInfo = {
7043
9144
  "components": {
7044
9145
  "Accordion": {
7045
9146
  "version": "1.1.0",
7046
- "lastUpdated": "2026-05-15",
9147
+ "lastUpdated": "2026-05-16",
7047
9148
  "breaking": false,
7048
9149
  "stable": true,
7049
9150
  "changelog": null
7050
9151
  },
7051
9152
  "Alert": {
7052
9153
  "version": "1.1.0",
7053
- "lastUpdated": "2026-05-15",
9154
+ "lastUpdated": "2026-05-16",
7054
9155
  "breaking": false,
7055
9156
  "stable": true,
7056
9157
  "changelog": null
7057
9158
  },
7058
9159
  "Avatar": {
7059
9160
  "version": "1.1.0",
7060
- "lastUpdated": "2026-05-15",
9161
+ "lastUpdated": "2026-05-16",
7061
9162
  "breaking": false,
7062
9163
  "stable": true,
7063
9164
  "changelog": null
7064
9165
  },
7065
9166
  "Badge": {
7066
9167
  "version": "1.1.0",
7067
- "lastUpdated": "2026-05-15",
9168
+ "lastUpdated": "2026-05-16",
7068
9169
  "breaking": false,
7069
9170
  "stable": true,
7070
9171
  "changelog": null
7071
9172
  },
7072
9173
  "Breadcrumb": {
7073
9174
  "version": "1.1.0",
7074
- "lastUpdated": "2026-05-15",
9175
+ "lastUpdated": "2026-05-16",
7075
9176
  "breaking": false,
7076
9177
  "stable": true,
7077
9178
  "changelog": null
7078
9179
  },
7079
9180
  "Button": {
7080
9181
  "version": "1.1.0",
7081
- "lastUpdated": "2026-05-15",
9182
+ "lastUpdated": "2026-05-16",
7082
9183
  "breaking": false,
7083
9184
  "stable": true,
7084
9185
  "changelog": null
7085
9186
  },
7086
9187
  "Calendar": {
7087
9188
  "version": "1.1.0",
7088
- "lastUpdated": "2026-05-15",
9189
+ "lastUpdated": "2026-05-16",
9190
+ "breaking": false,
9191
+ "stable": true,
9192
+ "changelog": null
9193
+ },
9194
+ "DatePicker": {
9195
+ "version": "1.1.0",
9196
+ "lastUpdated": "2026-05-16",
7089
9197
  "breaking": false,
7090
9198
  "stable": true,
7091
9199
  "changelog": null
7092
9200
  },
7093
9201
  "Card": {
7094
9202
  "version": "1.1.0",
7095
- "lastUpdated": "2026-05-15",
9203
+ "lastUpdated": "2026-05-16",
7096
9204
  "breaking": false,
7097
9205
  "stable": true,
7098
9206
  "changelog": null
7099
9207
  },
7100
9208
  "Checkbox": {
7101
9209
  "version": "1.1.0",
7102
- "lastUpdated": "2026-05-15",
9210
+ "lastUpdated": "2026-05-16",
9211
+ "breaking": false,
9212
+ "stable": true,
9213
+ "changelog": null
9214
+ },
9215
+ "RadioGroup": {
9216
+ "version": "1.1.0",
9217
+ "lastUpdated": "2026-05-16",
9218
+ "breaking": false,
9219
+ "stable": true,
9220
+ "changelog": null
9221
+ },
9222
+ "RichTextEditor": {
9223
+ "version": "1.1.0",
9224
+ "lastUpdated": "2026-05-16",
7103
9225
  "breaking": false,
7104
9226
  "stable": true,
7105
9227
  "changelog": null
7106
9228
  },
7107
9229
  "Command": {
7108
9230
  "version": "1.1.0",
7109
- "lastUpdated": "2026-05-15",
9231
+ "lastUpdated": "2026-05-16",
7110
9232
  "breaking": false,
7111
9233
  "stable": true,
7112
9234
  "changelog": null
7113
9235
  },
7114
9236
  "DropdownMenu": {
7115
9237
  "version": "1.1.0",
7116
- "lastUpdated": "2026-05-15",
9238
+ "lastUpdated": "2026-05-16",
7117
9239
  "breaking": false,
7118
9240
  "stable": true,
7119
9241
  "changelog": null
7120
9242
  },
7121
9243
  "Field": {
7122
9244
  "version": "1.1.0",
7123
- "lastUpdated": "2026-05-15",
9245
+ "lastUpdated": "2026-05-16",
7124
9246
  "breaking": false,
7125
9247
  "stable": true,
7126
9248
  "changelog": null
7127
9249
  },
7128
9250
  "Input": {
7129
9251
  "version": "1.1.0",
7130
- "lastUpdated": "2026-05-15",
9252
+ "lastUpdated": "2026-05-16",
7131
9253
  "breaking": false,
7132
9254
  "stable": true,
7133
9255
  "changelog": null
7134
9256
  },
7135
9257
  "Label": {
7136
9258
  "version": "1.1.0",
7137
- "lastUpdated": "2026-05-15",
9259
+ "lastUpdated": "2026-05-16",
7138
9260
  "breaking": false,
7139
9261
  "stable": true,
7140
9262
  "changelog": null
7141
9263
  },
7142
9264
  "Modal": {
7143
9265
  "version": "1.1.0",
7144
- "lastUpdated": "2026-05-15",
9266
+ "lastUpdated": "2026-05-16",
7145
9267
  "breaking": false,
7146
9268
  "stable": true,
7147
9269
  "changelog": null
7148
9270
  },
7149
9271
  "Pagination": {
7150
9272
  "version": "1.1.0",
7151
- "lastUpdated": "2026-05-15",
9273
+ "lastUpdated": "2026-05-16",
7152
9274
  "breaking": false,
7153
9275
  "stable": true,
7154
9276
  "changelog": null
7155
9277
  },
7156
9278
  "Popover": {
7157
9279
  "version": "1.1.0",
7158
- "lastUpdated": "2026-05-15",
9280
+ "lastUpdated": "2026-05-16",
7159
9281
  "breaking": false,
7160
9282
  "stable": true,
7161
9283
  "changelog": null
7162
9284
  },
7163
9285
  "Progress": {
7164
9286
  "version": "1.1.0",
7165
- "lastUpdated": "2026-05-15",
9287
+ "lastUpdated": "2026-05-16",
7166
9288
  "breaking": false,
7167
9289
  "stable": true,
7168
9290
  "changelog": null
7169
9291
  },
7170
9292
  "Select": {
7171
9293
  "version": "1.1.0",
7172
- "lastUpdated": "2026-05-15",
9294
+ "lastUpdated": "2026-05-16",
7173
9295
  "breaking": false,
7174
9296
  "stable": true,
7175
9297
  "changelog": null
7176
9298
  },
7177
9299
  "Separator": {
7178
9300
  "version": "1.1.0",
7179
- "lastUpdated": "2026-05-15",
9301
+ "lastUpdated": "2026-05-16",
7180
9302
  "breaking": false,
7181
9303
  "stable": true,
7182
9304
  "changelog": null
7183
9305
  },
7184
9306
  "Sheet": {
7185
9307
  "version": "1.1.0",
7186
- "lastUpdated": "2026-05-15",
9308
+ "lastUpdated": "2026-05-16",
7187
9309
  "breaking": false,
7188
9310
  "stable": true,
7189
9311
  "changelog": null
7190
9312
  },
7191
9313
  "Sidebar": {
7192
9314
  "version": "1.1.0",
7193
- "lastUpdated": "2026-05-15",
9315
+ "lastUpdated": "2026-05-16",
7194
9316
  "breaking": false,
7195
9317
  "stable": true,
7196
9318
  "changelog": null
7197
9319
  },
7198
9320
  "Skeleton": {
7199
9321
  "version": "1.1.0",
7200
- "lastUpdated": "2026-05-15",
9322
+ "lastUpdated": "2026-05-16",
7201
9323
  "breaking": false,
7202
9324
  "stable": true,
7203
9325
  "changelog": null
7204
9326
  },
7205
9327
  "Slider": {
7206
9328
  "version": "1.1.0",
7207
- "lastUpdated": "2026-05-15",
9329
+ "lastUpdated": "2026-05-16",
7208
9330
  "breaking": false,
7209
9331
  "stable": true,
7210
9332
  "changelog": null
7211
9333
  },
7212
9334
  "Switch": {
7213
9335
  "version": "1.1.0",
7214
- "lastUpdated": "2026-05-15",
9336
+ "lastUpdated": "2026-05-16",
7215
9337
  "breaking": false,
7216
9338
  "stable": true,
7217
9339
  "changelog": null
7218
9340
  },
7219
9341
  "Table": {
7220
9342
  "version": "1.1.0",
7221
- "lastUpdated": "2026-05-15",
9343
+ "lastUpdated": "2026-05-16",
7222
9344
  "breaking": false,
7223
9345
  "stable": true,
7224
9346
  "changelog": null
7225
9347
  },
7226
9348
  "Tabs": {
7227
9349
  "version": "1.1.0",
7228
- "lastUpdated": "2026-05-15",
9350
+ "lastUpdated": "2026-05-16",
7229
9351
  "breaking": false,
7230
9352
  "stable": true,
7231
9353
  "changelog": null
7232
9354
  },
7233
9355
  "Toast": {
7234
9356
  "version": "1.1.0",
7235
- "lastUpdated": "2026-05-15",
9357
+ "lastUpdated": "2026-05-16",
7236
9358
  "breaking": false,
7237
9359
  "stable": true,
7238
9360
  "changelog": null
7239
9361
  },
7240
9362
  "Tooltip": {
7241
9363
  "version": "1.1.0",
7242
- "lastUpdated": "2026-05-15",
9364
+ "lastUpdated": "2026-05-16",
7243
9365
  "breaking": false,
7244
9366
  "stable": true,
7245
9367
  "changelog": null
7246
9368
  },
7247
9369
  "Typography": {
7248
9370
  "version": "1.1.0",
7249
- "lastUpdated": "2026-05-15",
9371
+ "lastUpdated": "2026-05-16",
7250
9372
  "breaking": false,
7251
9373
  "stable": true,
7252
9374
  "changelog": null
7253
9375
  },
7254
9376
  "ButtonGroup": {
7255
9377
  "version": "1.1.0",
7256
- "lastUpdated": "2026-05-15",
9378
+ "lastUpdated": "2026-05-16",
7257
9379
  "breaking": false,
7258
9380
  "stable": true,
7259
9381
  "changelog": null
7260
9382
  },
7261
9383
  "TagsInput": {
7262
9384
  "version": "1.1.0",
7263
- "lastUpdated": "2026-05-15",
9385
+ "lastUpdated": "2026-05-16",
7264
9386
  "breaking": false,
7265
9387
  "stable": true,
7266
9388
  "changelog": null
7267
9389
  },
7268
9390
  "Combobox": {
7269
9391
  "version": "1.1.0",
7270
- "lastUpdated": "2026-05-15",
9392
+ "lastUpdated": "2026-05-16",
7271
9393
  "breaking": false,
7272
9394
  "stable": true,
7273
9395
  "changelog": null
7274
9396
  },
7275
9397
  "DataGrid": {
7276
9398
  "version": "1.1.0",
7277
- "lastUpdated": "2026-05-15",
9399
+ "lastUpdated": "2026-05-16",
7278
9400
  "breaking": false,
7279
9401
  "stable": true,
7280
9402
  "changelog": null
7281
9403
  },
7282
9404
  "Tag": {
7283
9405
  "version": "1.1.0",
7284
- "lastUpdated": "2026-05-15",
9406
+ "lastUpdated": "2026-05-16",
7285
9407
  "breaking": false,
7286
9408
  "stable": true,
7287
9409
  "changelog": null
7288
9410
  },
7289
9411
  "TagGroup": {
7290
9412
  "version": "1.1.0",
7291
- "lastUpdated": "2026-05-15",
9413
+ "lastUpdated": "2026-05-16",
9414
+ "breaking": false,
9415
+ "stable": true,
9416
+ "changelog": null
9417
+ },
9418
+ "Item": {
9419
+ "version": "1.1.0",
9420
+ "lastUpdated": "2026-05-16",
9421
+ "breaking": false,
9422
+ "stable": true,
9423
+ "changelog": null
9424
+ },
9425
+ "Kanban": {
9426
+ "version": "1.1.0",
9427
+ "lastUpdated": "2026-05-16",
9428
+ "breaking": false,
9429
+ "stable": true,
9430
+ "changelog": null
9431
+ },
9432
+ "Conversation": {
9433
+ "version": "1.1.0",
9434
+ "lastUpdated": "2026-05-16",
9435
+ "breaking": false,
9436
+ "stable": true,
9437
+ "changelog": null
9438
+ },
9439
+ "DateRangePicker": {
9440
+ "version": "1.1.0",
9441
+ "lastUpdated": "2026-05-16",
7292
9442
  "breaking": false,
7293
9443
  "stable": true,
7294
9444
  "changelog": null
@@ -7303,13 +9453,16 @@ export const cheatSheets = {
7303
9453
  "Badge": "# Badge — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Small label for status, category, or count. Use dot prop for live status indicator.**\n\n```ts\nimport { Badge } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `variant` | `\"default\"` \\| `\"accent\"` \\| `\"teal\"` \\| `\"success\"` \\| `\"warning\"` \\| `\"danger\"` | `default` |\n| `dot` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `variant=\"primary\"`\n → Use variant=\"accent\" for brand color\n- ❌ `variant=\"error\"`\n → Use variant=\"danger\"\n- ❌ `variant=\"info\"`\n → Use variant=\"teal\" for info-like styling\n\n## Examples\n\n**Live status with dot**\n```tsx\n<Badge variant=\"success\" dot>Online</Badge>\n```\n\n**Warning badge**\n```tsx\n<Badge variant=\"warning\">Beta</Badge>\n```\n\n**Danger badge**\n```tsx\n<Badge variant=\"danger\">Error</Badge>\n```\n",
7304
9454
  "Breadcrumb": "# Breadcrumb — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Navigation trail showing current page location in hierarchy.**\n\n```ts\nimport { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from \"@usevyre/react\"\n```\n\n## Common AI Mistakes\n\n- ❌ `Using plain <a> tags inside Breadcrumb`\n → Use BreadcrumbItem > BreadcrumbLink for each crumb\n\n## Examples\n\n**Basic breadcrumb**\n```tsx\n<Breadcrumb>\n <BreadcrumbItem><BreadcrumbLink href=\"/\">Home</BreadcrumbLink></BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem><BreadcrumbLink href=\"/docs\">Docs</BreadcrumbLink></BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem aria-current=\"page\">Button</BreadcrumbItem>\n</Breadcrumb>\n```\n",
7305
9455
  "Button": "# Button — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Triggers actions and navigation. The most commonly used interactive element.**\n\n```ts\nimport { Button } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `variant` | `\"primary\"` \\| `\"secondary\"` \\| `\"ghost\"` \\| `\"accent\"` \\| `\"teal\"` \\| `\"danger\"` | `secondary` |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `\"icon\"` | `md` |\n| `loading` | `true` \\| `false` | `false` |\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `variant=\"blue\"`\n → Use variant=\"accent\" for brand amber, or variant=\"teal\" for teal\n- ❌ `size=\"xl\"`\n → Use size=\"lg\"\n- ❌ `color=\"...\"`\n → Use variant prop instead\n- ❌ `icon={...}`\n → Use leftIcon={...} or rightIcon={...}\n- ❌ `size=\"icon\" without aria-label`\n → Add aria-label describing the action\n\n## Examples\n\n**Primary CTA**\n```tsx\n<Button variant=\"primary\">Get Started</Button>\n```\n\n**Large accent button**\n```tsx\n<Button variant=\"accent\" size=\"lg\">Launch App</Button>\n```\n\n**Loading state**\n```tsx\n<Button variant=\"danger\" loading>Deleting...</Button>\n```\n\n**Link button**\n```tsx\n<Button as=\"a\" href=\"/docs\" variant=\"secondary\">Read Docs</Button>\n```\n\n**Icon-only button**\n```tsx\n<Button variant=\"ghost\" size=\"icon\" aria-label=\"Close\">\n <X size={16} />\n</Button>\n```\n\n## Accessibility\n\n- Always add aria-label when using size='icon' (no visible text)\n",
7306
- "Calendar": "# Calendar — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Date picker calendar widget for selecting single dates or ranges.**\n\n```ts\nimport { Calendar } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Using Calendar for time selection`\n → Combine with a separate time Input if time selection is needed\n\n## Examples\n\n**Controlled date picker**\n```tsx\nconst [date, setDate] = useState(null);\n<Calendar value={date} onChange={setDate} />\n```\n",
9456
+ "Calendar": "# Calendar — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Inline date-grid widget (always visible, no input). mode: single | range | multiple, optional time picker. For an input + popover use DatePicker; for start/end ranges with presets use DateRangePicker.**\n\n```ts\nimport { Calendar } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Calendar for an input field that opens a popover`\n → Use <DatePicker /> (single date) or <DateRangePicker /> (range)\n- `value as tuple for mode=\"single\"`\n → Pass value matching mode; use mode=\"range\" for [start,end]\n\n## Examples\n\n**Controlled date picker**\n```tsx\nconst [date, setDate] = useState(null);\n<Calendar value={date} onChange={setDate} />\n```\n",
9457
+ "DatePicker": "# DatePicker — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Input trigger that opens a Calendar in a popover. Same modes as Calendar (single | range | multiple) plus a placeholder. Use this for a compact date field; use Calendar for an always-visible grid, or DateRangePicker for start/end ranges with presets.**\n\n```ts\nimport { DatePicker } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `mode` | `\"single\"` \\| `\"range\"` \\| `\"multiple\"` | `single` |\n| `weekStartsOn` | `\"0\"` \\| `\"1\"` | `1` |\n| `showTime` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `DatePicker mode=\"range\" for { from, to } object`\n → Use <DateRangePicker /> for the { from, to } object API + presets + dual month\n- ❌ `DatePicker without value/onChange`\n → Provide value and onChange (e.g. from useState)\n\n## Examples\n\n**Single date field**\n```tsx\nconst [date, setDate] = useState(null);\n<DatePicker value={date} onChange={setDate} placeholder=\"Pick a date\" />\n```\n\n**Date + time**\n```tsx\n<DatePicker value={date} onChange={setDate} showTime />\n```\n",
7307
9458
  "Card": "# Card — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Content container with optional header, body, and footer sections.**\n\n```ts\nimport { Card, CardHeader, CardBody, CardFooter } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `variant` | `\"default\"` \\| `\"elevated\"` \\| `\"outlined\"` \\| `\"ghost\"` \\| `\"accent\"` | `default` |\n| `hoverable` | `true` \\| `false` | `false` |\n| `clickable` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `variant=\"primary\"`\n → Use variant=\"elevated\" | \"outlined\" | \"ghost\" | \"accent\"\n\n## Examples\n\n**Elevated card with sections**\n```tsx\n<Card variant=\"elevated\">\n <CardHeader><Badge variant=\"teal\">New</Badge></CardHeader>\n <CardBody>\n <h3>Card Title</h3>\n <p>Description text.</p>\n </CardBody>\n <CardFooter>\n <Button variant=\"ghost\" size=\"sm\">Learn more</Button>\n </CardFooter>\n</Card>\n```\n",
7308
9459
  "Checkbox": "# Checkbox — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Binary toggle for boolean form values.**\n\n```ts\nimport { Checkbox } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` | `md` |\n| `checked` | `true` \\| `false` | — |\n| `disabled` | `true` \\| `false` | `false` |\n| `indeterminate` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `size=\"lg\"`\n → Use size=\"md\"\n\n## Examples\n\n**Labeled checkbox**\n```tsx\n<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--vyre-spacing-2)' }}>\n <Checkbox checked={agreed} onChange={e => setAgreed(e.target.checked)} />\n I agree to the terms\n</label>\n```\n",
9460
+ "RadioGroup": "# RadioGroup — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Controlled single-choice group. RadioGroup owns the selected value; render it data-driven via the options array OR with composable <Radio> children for custom content. role=radiogroup with proper labelling. For multi-select use Checkbox; for a compact dropdown use Select.**\n\n```ts\nimport { RadioGroup, Radio } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` | `md` |\n| `orientation` | `\"vertical\"` \\| `\"horizontal\"` | `vertical` |\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `<Radio> used outside a <RadioGroup>`\n → Always wrap <Radio> in <RadioGroup>\n- ❌ `RadioGroup without value/onChange (React) or v-model (Vue)`\n → Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React\n- ❌ `Using Checkbox for mutually-exclusive choices`\n → Use RadioGroup + Radio (or options) for one-of-many\n\n## Examples\n\n**Data-driven**\n```tsx\n<RadioGroup\n value={plan}\n onChange={setPlan}\n options={[\n { value: \"free\", label: \"Free\", description: \"For hobby projects\" },\n { value: \"pro\", label: \"Pro\", description: \"For teams\" },\n ]}\n/>\n```\n\n**Composable children**\n```tsx\n<RadioGroup value={plan} onChange={setPlan} orientation=\"horizontal\">\n <Radio value=\"free\" label=\"Free\" />\n <Radio value=\"pro\" label=\"Pro\" />\n</RadioGroup>\n```\n",
9461
+ "RichTextEditor": "# RichTextEditor — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Controlled WYSIWYG editor. value is an HTML string; you own it in state and set it in onChange (React) / v-model (Vue). Native contentEditable + execCommand — zero dependencies. Toolbar: bold, italic, underline, strike, h1-h3, ordered/unordered lists, quote, code block, link, clear formatting.**\n\n```ts\nimport { RichTextEditor } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `disabled` | `true` \\| `false` | `false` |\n| `readOnly` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `RichTextEditor without value/onChange (React) or v-model (Vue)`\n → Keep the HTML string in state and update it in onChange / v-model\n- ❌ `Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising`\n → Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output\n- ❌ `toolbar=\"bold\" (string)`\n → Pass an array, e.g. toolbar={[\"bold\",\"italic\",\"link\"]}\n\n## Examples\n\n**Controlled editor**\n```tsx\nconst [html, setHtml] = useState(\"<p>Hello <strong>world</strong></p>\");\n<RichTextEditor value={html} onChange={setHtml} placeholder=\"Write…\" />\n```\n\n**Minimal toolbar**\n```tsx\n<RichTextEditor\n value={html}\n onChange={setHtml}\n toolbar={[\"bold\", \"italic\", \"link\"]}\n/>\n```\n",
7309
9462
  "Command": "# Command — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Command palette / search dialog. Use for search-first navigation or quick actions.**\n\n```ts\nimport { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandDialog } from \"@usevyre/react\"\n```\n\n## Common AI Mistakes\n\n- ❌ `Using Input type=\"search\" for search UI`\n → Use Command + CommandInput + CommandList + CommandItem\n\n## Examples\n\n**Basic command palette**\n```tsx\n<Command>\n <CommandInput placeholder=\"Search...\" />\n <CommandList>\n <CommandEmpty>No results found.</CommandEmpty>\n <CommandGroup heading=\"Suggestions\">\n <CommandItem onSelect={() => handleSelect('dashboard')}>Dashboard</CommandItem>\n <CommandItem onSelect={() => handleSelect('settings')}>Settings</CommandItem>\n </CommandGroup>\n </CommandList>\n</Command>\n```\n",
7310
9463
  "DropdownMenu": "# DropdownMenu — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Contextual menu triggered by a button. Supports items, separators, checkbox items, radio groups, and sub-menus.**\n\n```ts\nimport { DropdownMenu, DropdownItem, DropdownSeparator, DropdownCheckboxItem, DropdownRadioGroup, DropdownRadioItem, DropdownSub } from \"@usevyre/react\"\n```\n\n## Common AI Mistakes\n\n- ❌ `DropdownItem variant=\"primary\"`\n → Use variant=\"danger\" for destructive items only\n\n## Examples\n\n**Dropdown with danger item**\n```tsx\n<DropdownMenu trigger={<Button variant=\"secondary\">Options</Button>}>\n <DropdownItem onSelect={() => handleEdit()}>Edit</DropdownItem>\n <DropdownItem onSelect={() => handleDuplicate()}>Duplicate</DropdownItem>\n <DropdownSeparator />\n <DropdownItem variant=\"danger\" onSelect={() => handleDelete()}>Delete</DropdownItem>\n</DropdownMenu>\n```\n",
7311
- "Field": "# Field — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Form field wrapper providing label, hint text, and validation state for Input or Textarea.**\n\n```ts\nimport { Field, Input, Textarea } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `state` | `\"idle\"` \\| `\"error\"` \\| `\"success\"` \\| `\"warning\"` | `idle` |\n| `required` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Applying state prop directly to Input`\n → Wrap Input in <Field state=\"error\"> to apply validation styling\n\n## Examples\n\n**Error state field**\n```tsx\n<Field label=\"Email\" state=\"error\" hint=\"Invalid email format\">\n <Input type=\"email\" placeholder=\"you@example.com\" />\n</Field>\n```\n\n**Search field with left icon**\n```tsx\n<Field label=\"Search\">\n <Input leftElement={<SearchIcon />} placeholder=\"Search...\" />\n</Field>\n```\n",
7312
- "Input": "# Input — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Text input field. Wrap in Field for labels and validation. Use leftElement/rightElement for icons.**\n\n```ts\nimport { Input } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n\n## Common AI Mistakes\n\n- ❌ `size=\"icon\"`\n → Use size=\"sm\" | \"md\" | \"lg\"\n- ❌ `type=\"search\" for search UI`\n → Import Command from @usevyre/react for search palettes\n\n## Examples\n\n**Password input with icon**\n```tsx\n<Input type=\"password\" rightElement={<EyeIcon />} placeholder=\"Password\" />\n```\n",
9464
+ "Field": "# Field — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Form field wrapper. Two ways to use it (both supported): (1) props-based — pass label/hint/state/required for the common case; (2) composable — use the parts FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSet for richer layouts (multiple controls, custom error placement). The props-based API is unchanged and still works.**\n\n```ts\nimport { Field, Input, Textarea } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `state` | `\"idle\"` \\| `\"error\"` \\| `\"success\"` \\| `\"warning\"` | `idle` |\n| `required` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Applying state prop directly to Input`\n → Wrap Input in <Field state=\"error\"> to apply validation styling\n- ❌ `Mixing props label/hint AND FieldLabel/FieldError for the same field`\n → Pick one: either props-based (label/hint/state) OR composable parts\n\n## Examples\n\n**Error state field**\n```tsx\n<Field label=\"Email\" state=\"error\" hint=\"Invalid email format\">\n <Input type=\"email\" placeholder=\"you@example.com\" />\n</Field>\n```\n\n**Search field with left icon**\n```tsx\n<Field label=\"Search\">\n <Input leftElement={<SearchIcon />} placeholder=\"Search...\" />\n</Field>\n```\n\n**Composable field with explicit parts**\n```tsx\n<Field>\n <FieldLabel required htmlFor=\"email\">Email</FieldLabel>\n <Input id=\"email\" type=\"email\" />\n <FieldDescription>We\\u2019ll never share it.</FieldDescription>\n <FieldError>{errors.email}</FieldError>\n</Field>\n\n// Two controls side by side\n<FieldGroup orientation=\"horizontal\">\n <Field label=\"First name\"><Input /></Field>\n <Field label=\"Last name\"><Input /></Field>\n</FieldGroup>\n```\n",
9465
+ "Input": "# Input — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Text input field. Wrap in Field for labels and validation. Use leftElement/rightElement for icons.**\n\n```ts\nimport { Input } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n\n## Common AI Mistakes\n\n- ❌ `size=\"icon\"`\n → Use size=\"sm\" | \"md\" | \"lg\"\n- ❌ `type=\"search\" for search UI`\n → Import Command from @usevyre/react for search palettes\n- ❌ `Vue: binding Input/Textarea value without v-model`\n → Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange\n\n## Examples\n\n**Password input with icon**\n```tsx\n<Input type=\"password\" rightElement={<EyeIcon />} placeholder=\"Password\" />\n```\n",
7313
9466
  "Label": "# Label — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Accessible form label. Associate with input via htmlFor.**\n\n```ts\nimport { Label } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `required` | `true` \\| `false` | `false` |\n\n## Examples\n\n**Label with input**\n```tsx\n<Label htmlFor=\"email\">Email address</Label>\n<Input id=\"email\" type=\"email\" />\n```\n",
7314
9467
  "Modal": "# Modal — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Dialog overlay for confirmations, forms, or focused content.**\n\n```ts\nimport { Modal, ModalHeader, ModalBody, ModalFooter } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `\"full\"` | `md` |\n| `open` | `true` \\| `false` | — |\n\n## Common AI Mistakes\n\n- ❌ `size=\"xl\"`\n → Use size=\"lg\" or size=\"full\"\n\n## Examples\n\n**Confirmation modal**\n```tsx\n<Modal open={isOpen} onClose={() => setIsOpen(false)} title=\"Confirm Delete\" size=\"sm\">\n <ModalBody>Are you sure you want to delete this item?</ModalBody>\n <ModalFooter>\n <Button variant=\"ghost\" onClick={() => setIsOpen(false)}>Cancel</Button>\n <Button variant=\"danger\" onClick={handleDelete}>Delete</Button>\n </ModalFooter>\n</Modal>\n```\n",
7315
9468
  "Pagination": "# Pagination — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Page navigation for paginated lists or tables.**\n\n```ts\nimport { Pagination } from \"@usevyre/react\"\n```\n\n## Examples\n\n**Basic pagination**\n```tsx\n<Pagination page={currentPage} total={totalPages} onChange={setCurrentPage} />\n```\n",
@@ -7332,5 +9485,9 @@ export const cheatSheets = {
7332
9485
  "Combobox": "# Combobox — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Searchable single-select dropdown with typeahead filtering and keyboard navigation. Use when the list is long enough to need search. Differs from Select (no search) and Command (palette).**\n\n```ts\nimport { Combobox } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Combobox value=\"\"`\n → Use value={null} for no selection\n- ❌ `Combobox options={string[]}`\n → Use [{ value: 'ts', label: 'TypeScript' }]\n- ❌ `Using Combobox for command palette`\n → Use Command for command palettes\n\n## Examples\n\n**Searchable language picker**\n```tsx\nconst [lang, setLang] = useState<string | null>(null);\n<Combobox\n options={[{ value: \"ts\", label: \"TypeScript\" }, { value: \"go\", label: \"Go\" }]}\n value={lang}\n onChange={setLang}\n placeholder=\"Search language…\"\n/>\n```\n",
7333
9486
  "DataGrid": "# DataGrid — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Table with built-in column sorting, loading skeletons, and empty state. Filtering and pagination are out of scope — compose with the Pagination component.**\n\n```ts\nimport { DataGrid } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `sortDir` | `\"asc\"` \\| `\"desc\"` | — |\n| `loading` | `true` \\| `false` | `false` |\n| `stickyHeader` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `DataGrid expecting built-in pagination`\n → Slice rows yourself and use the Pagination component\n- ❌ `DataGrid expecting built-in filtering`\n → Filter the rows array before passing it in\n- ❌ `sortable without onSort`\n → Handle onSort and sort the rows array in your state\n\n## Examples\n\n**Sortable grid**\n```tsx\nconst cols = [{ key: \"name\", label: \"Name\", sortable: true }];\n<DataGrid\n columns={cols}\n rows={people}\n sortKey={sortKey}\n sortDir={sortDir}\n onSort={(k, d) => { setSortKey(k); setSortDir(d); }}\n/>\n```\n\n**Loading state**\n```tsx\n<DataGrid columns={cols} rows={[]} loading />\n```\n",
7334
9487
  "Tag": "# Tag — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Standalone display tag/chip for categories, labels, or filter chips. NOT an input — for tag input use TagsInput. Group multiple with TagGroup.**\n\n```ts\nimport { Tag } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `variant` | `\"default\"` \\| `\"accent\"` \\| `\"danger\"` | `default` |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n| `disabled` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Tag variant=\"success\"`\n → Use Badge for success/warning/teal status colors; Tag is for categories/filters\n- ❌ `Using Tag for tag input`\n → Use TagsInput for adding/removing tags via keyboard\n- ❌ `Tag size=\"xl\"`\n → Use size=\"lg\"\n\n## Examples\n\n**Category tags in a group**\n```tsx\n<TagGroup>\n <Tag>Design</Tag>\n <Tag variant=\"accent\">Featured</Tag>\n <Tag>Engineering</Tag>\n</TagGroup>\n```\n\n**Removable filter chip (React)**\n```tsx\n<Tag onRemove={() => removeFilter(\"react\")}>react</Tag>\n```\n\n**Clickable toggle tag (React)**\n```tsx\n<Tag onClick={() => toggleFilter(\"vue\")}>vue</Tag>\n```\n",
7335
- "TagGroup": "# TagGroup — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Read-only container that lays out multiple Tag elements with automatic wrapping and consistent spacing. For tag input use TagsInput.**\n\n```ts\nimport { TagGroup, Tag } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `gap` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n| `wrap` | `true` \\| `false` | `true` |\n\n## Common AI Mistakes\n\n- ❌ `TagGroup without Tag children`\n → Place <Tag> elements as direct children\n- ❌ `Using TagGroup for tag input`\n → Use TagsInput for an editable tag field\n\n## Examples\n\n**Tag group with mixed variants**\n```tsx\n<TagGroup gap=\"sm\">\n <Tag>React</Tag>\n <Tag>Vue</Tag>\n <Tag variant=\"accent\">TypeScript</Tag>\n</TagGroup>\n```\n"
9488
+ "TagGroup": "# TagGroup — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Read-only container that lays out multiple Tag elements with automatic wrapping and consistent spacing. For tag input use TagsInput.**\n\n```ts\nimport { TagGroup, Tag } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `gap` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n| `wrap` | `true` \\| `false` | `true` |\n\n## Common AI Mistakes\n\n- ❌ `TagGroup without Tag children`\n → Place <Tag> elements as direct children\n- ❌ `Using TagGroup for tag input`\n → Use TagsInput for an editable tag field\n\n## Examples\n\n**Tag group with mixed variants**\n```tsx\n<TagGroup gap=\"sm\">\n <Tag>React</Tag>\n <Tag>Vue</Tag>\n <Tag variant=\"accent\">TypeScript</Tag>\n</TagGroup>\n```\n",
9489
+ "Item": "# Item — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.**\n\n```ts\nimport { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `variant` | `\"default\"` \\| `\"outlined\"` \\| `\"muted\"` \\| `\"plain\"` | `default` |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `md` |\n| `clickable` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Card used for repeated list rows`\n → Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows\n- ❌ `Item variant=\"primary\"`\n → Use variant=\"default\" | \"outlined\" | \"muted\"\n- ❌ `raw text directly inside Item`\n → Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>\n\n## Examples\n\n**Settings row with media, content and a trailing switch**\n```tsx\n<Item>\n <ItemMedia><BellIcon /></ItemMedia>\n <ItemContent>\n <ItemTitle>Notifications</ItemTitle>\n <ItemDescription>Receive an email when someone mentions you.</ItemDescription>\n </ItemContent>\n <ItemActions>\n <Switch defaultChecked />\n </ItemActions>\n</Item>\n```\n\n**Grouped clickable list with dividers**\n```tsx\n<ItemGroup separated>\n <Item clickable>\n <ItemContent><ItemTitle>Profile</ItemTitle></ItemContent>\n </Item>\n <Item clickable>\n <ItemContent><ItemTitle>Billing</ItemTitle></ItemContent>\n </Item>\n</ItemGroup>\n```\n",
9490
+ "Kanban": "# Kanban — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Drag-and-drop board: cards move between columns (or reorder within a column). CONTROLLED & data-driven like DataGrid. While dragging, a placeholder shows the exact drop position. Each card is wrapped in a Card (variant=\"outlined\"); renderCard (React) / #card slot (Vue) can render ANY content incl. complex components (Avatar/Badge/Progress). Columns and cards accept an optional semantic color tint. Native HTML5 DnD, zero deps.**\n\n```ts\nimport { Kanban } from \"@usevyre/react\"\n```\n\n## Common AI Mistakes\n\n- ❌ `Kanban without onChange (or ignoring it)`\n → Store columns in state and setColumns in onChange (v-model in Vue)\n- ❌ `Duplicate card ids across columns`\n → Use globally-unique card ids across the entire board\n- ❌ `Mutating value in place then calling onChange`\n → Pass the new array Kanban gives you straight to setState / v-model\n- ❌ `color=\"blue\" (or any non-semantic value)`\n → Use one of: \"default\" | \"accent\" | \"teal\" | \"success\" | \"warning\" | \"danger\"\n\n## Examples\n\n**Controlled board**\n```tsx\nconst [columns, setColumns] = useState([\n { id: \"todo\", title: \"To Do\", cards: [{ id: \"1\", title: \"Spec API\" }] },\n { id: \"doing\", title: \"In Progress\", cards: [] },\n { id: \"done\", title: \"Done\", cards: [{ id: \"2\", title: \"Kickoff\" }] },\n]);\n<Kanban value={columns} onChange={setColumns} />\n```\n\n**Custom card body + click handler**\n```tsx\n<Kanban\n value={columns}\n onChange={setColumns}\n onCardClick={(card) => openDetail(card.id)}\n renderCard={(card) => (\n <><strong>{card.title}</strong><Badge>{card.id}</Badge></>\n )}\n/>\n```\n\n**Tinted columns/cards + complex card content**\n```tsx\nconst [cols, setCols] = useState([\n { id: \"doing\", title: \"In Progress\", color: \"teal\", cards: [\n { id: \"t1\", title: \"OAuth\", assignee: \"AK\", progress: 60, color: \"warning\" },\n ]},\n]);\n<Kanban\n value={cols}\n onChange={setCols}\n renderCard={(card) => (\n <><strong>{card.title}</strong><Progress value={card.progress} /></>\n )}\n/>\n```\n",
9491
+ "Conversation": "# Conversation — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Chat / inbox message thread. CONTROLLED & data-driven like Kanban — you own `value` (messages array) and append in your own send handler; Conversation holds no message state. Consecutive messages from the same author are grouped (avatar + name shown once), day separators are inserted on date change, and outgoing messages (authorId === currentUserId) align right.**\n\n```ts\nimport { Conversation } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `composer` | `true` \\| `false` | `false` |\n| `allowAttachments` | `true` \\| `false` | `false` |\n\n## Common AI Mistakes\n\n- ❌ `Conversation without currentUserId`\n → Always pass currentUserId matching one of the message authorId values\n- ❌ `Expecting Conversation to store/append messages`\n → Append to your own state in onSend (or @send) and pass it back via value\n- ❌ `composer without onSend (React) / @send (Vue)`\n → Provide onSend / @send to append the message to value\n- ❌ `Treating onSend as (text) only when using allowAttachments`\n → Handle onSend(text, files) — map files to message attachments and append\n\n## Examples\n\n**Controlled thread with built-in composer**\n```tsx\nconst [messages, setMessages] = useState([\n { id: \"1\", authorId: \"sam\", authorName: \"Sam\", text: \"Hey!\" },\n { id: \"2\", authorId: \"me\", text: \"Hi \\ud83d\\udc4b\", status: \"read\" },\n]);\n<Conversation\n value={messages}\n currentUserId=\"me\"\n composer\n onSend={(t) => setMessages((m) => [...m, { id: crypto.randomUUID(), authorId: \"me\", text: t }])}\n/>\n```\n\n**Typing indicator + custom bubble**\n```tsx\n<Conversation\n value={messages}\n currentUserId=\"me\"\n typing=\"Sam is typing\"\n renderMessage={(m) => <strong>{m.text}</strong>}\n/>\n```\n\n**Message with image + file attachments**\n```tsx\nconst messages = [\n { id: \"1\", authorId: \"sam\", authorName: \"Sam\", text: \"Moodboard \\ud83d\\udc47\",\n attachments: [{ kind: \"image\", url: \"/board.png\", name: \"board.png\" }] },\n { id: \"2\", authorId: \"me\", text: \"Specs:\", status: \"read\",\n attachments: [{ kind: \"file\", url: \"/spec.pdf\", name: \"spec.pdf\", size: \"2.4 MB\" }] },\n];\n<Conversation value={messages} currentUserId=\"me\" />\n```\n",
9492
+ "DateRangePicker": "# DateRangePicker — AI Cheat Sheet\n> Quick reference for Claude / Cursor / Copilot\n\n**Start/end date range picker. Built on Calendar (mode=range) with a friendlier { from, to } object API, a two-month side-by-side view, and preset shortcuts. Use this for report/filter date ranges; use DatePicker for a single date.**\n\n```ts\nimport { DateRangePicker } from \"@usevyre/react\"\n```\n\n## Valid Props\n\n| Prop | Values | Default |\n|------|--------|---------|\n| `numberOfMonths` | `\"1\"` \\| `\"2\"` | `2` |\n| `weekStartsOn` | `\"0\"` \\| `\"1\"` | `1` |\n\n## Common AI Mistakes\n\n- ❌ `value={[from, to]}`\n → Use value={{ from, to }} and read range.from / range.to\n- ❌ `DateRangePicker for a single date`\n → Use <DatePicker /> for a single date\n- ❌ `presets=\"true\" (string)`\n → Use the bare prop: presets (or presets={true})\n\n## Examples\n\n**Range picker with built-in presets**\n```tsx\nconst [range, setRange] = useState({ from: null, to: null });\n<DateRangePicker value={range} onChange={setRange} presets />\n```\n\n**Single month, no presets**\n```tsx\n<DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />\n```\n"
7336
9493
  };