@usevyre/ai-context 1.0.1 → 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/anti-patterns.json +284 -4
- package/dist/cheat-sheets/buttongroup.md +42 -0
- package/dist/cheat-sheets/calendar.md +5 -3
- package/dist/cheat-sheets/combobox.md +37 -0
- package/dist/cheat-sheets/conversation.md +63 -0
- package/dist/cheat-sheets/datagrid.md +44 -0
- package/dist/cheat-sheets/datepicker.md +36 -0
- package/dist/cheat-sheets/daterangepicker.md +37 -0
- package/dist/cheat-sheets/field.md +19 -1
- package/dist/cheat-sheets/index.md +15 -2
- package/dist/cheat-sheets/input.md +2 -0
- package/dist/cheat-sheets/item.md +53 -0
- package/dist/cheat-sheets/kanban.md +59 -0
- package/dist/cheat-sheets/radiogroup.md +47 -0
- package/dist/cheat-sheets/richtexteditor.md +41 -0
- package/dist/cheat-sheets/tag.md +46 -0
- package/dist/cheat-sheets/taggroup.md +33 -0
- package/dist/cheat-sheets/tagsinput.md +35 -0
- package/dist/claude-context.md +521 -5
- package/dist/copilot-instructions.md +521 -5
- package/dist/cursor-rules.md +164 -4
- package/dist/full-context.md +520 -4
- package/dist/index.js +3862 -325
- package/dist/schema.json +929 -10
- package/dist/tokens.json +1 -1
- package/dist/tokens.md +1 -1
- package/dist/version-info.json +166 -65
- package/dist/windsurf-rules.md +521 -5
- package/package.json +1 -1
package/dist/schema.json
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"generatedAt": "2026-05-16",
|
|
5
5
|
"package": "@usevyre/react",
|
|
6
|
-
"packageVersion": "1.
|
|
6
|
+
"packageVersion": "1.1.0",
|
|
7
7
|
"validFor": [
|
|
8
|
-
"@usevyre/react@1.
|
|
9
|
-
"@usevyre/vue@1.
|
|
8
|
+
"@usevyre/react@1.1.0+",
|
|
9
|
+
"@usevyre/vue@1.1.0+"
|
|
10
10
|
],
|
|
11
11
|
"changelog": {
|
|
12
|
+
"1.2.0": {
|
|
13
|
+
"date": "2026-05-16",
|
|
14
|
+
"breaking": false,
|
|
15
|
+
"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"
|
|
16
|
+
},
|
|
17
|
+
"1.1.0": {
|
|
18
|
+
"date": "2026-05-15",
|
|
19
|
+
"breaking": false,
|
|
20
|
+
"summary": "Added 6 components: ButtonGroup, TagsInput, Combobox, DataGrid, Tag, TagGroup"
|
|
21
|
+
},
|
|
12
22
|
"1.0.0": {
|
|
13
23
|
"date": "2026-05-13",
|
|
14
24
|
"breaking": true,
|
|
@@ -344,7 +354,7 @@
|
|
|
344
354
|
]
|
|
345
355
|
},
|
|
346
356
|
"Calendar": {
|
|
347
|
-
"description": "
|
|
357
|
+
"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.",
|
|
348
358
|
"import": "import { Calendar } from \"@usevyre/react\"",
|
|
349
359
|
"props": {
|
|
350
360
|
"value": {
|
|
@@ -358,13 +368,22 @@
|
|
|
358
368
|
"disabled": {
|
|
359
369
|
"type": "boolean",
|
|
360
370
|
"default": false
|
|
371
|
+
},
|
|
372
|
+
"defaultMonth": {
|
|
373
|
+
"type": "Date",
|
|
374
|
+
"description": "Month to display initially when value is empty (uncontrolled view). Used by DateRangePicker for the second month."
|
|
361
375
|
}
|
|
362
376
|
},
|
|
363
377
|
"antiPatterns": [
|
|
364
378
|
{
|
|
365
|
-
"pattern": "
|
|
366
|
-
"reason": "Calendar
|
|
367
|
-
"fix": "
|
|
379
|
+
"pattern": "Calendar for an input field that opens a popover",
|
|
380
|
+
"reason": "Calendar renders an always-visible grid, not a trigger",
|
|
381
|
+
"fix": "Use <DatePicker /> (single date) or <DateRangePicker /> (range)"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"pattern": "value as tuple for mode=\"single\"",
|
|
385
|
+
"reason": "value type must match mode: Date for single, [Date,Date] for range, Date[] for multiple",
|
|
386
|
+
"fix": "Pass value matching mode; use mode=\"range\" for [start,end]"
|
|
368
387
|
}
|
|
369
388
|
],
|
|
370
389
|
"examples": [
|
|
@@ -374,6 +393,87 @@
|
|
|
374
393
|
}
|
|
375
394
|
]
|
|
376
395
|
},
|
|
396
|
+
"DatePicker": {
|
|
397
|
+
"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.",
|
|
398
|
+
"import": "import { DatePicker } from \"@usevyre/react\"",
|
|
399
|
+
"props": {
|
|
400
|
+
"value": {
|
|
401
|
+
"type": "Date | [Date, Date] | Date[] | null",
|
|
402
|
+
"description": "Selected value (controlled). Type must match mode."
|
|
403
|
+
},
|
|
404
|
+
"onChange": {
|
|
405
|
+
"type": "function",
|
|
406
|
+
"description": "Callback when the selection changes; argument shape matches mode"
|
|
407
|
+
},
|
|
408
|
+
"mode": {
|
|
409
|
+
"type": "enum",
|
|
410
|
+
"values": [
|
|
411
|
+
"single",
|
|
412
|
+
"range",
|
|
413
|
+
"multiple"
|
|
414
|
+
],
|
|
415
|
+
"default": "single",
|
|
416
|
+
"description": "Selection mode"
|
|
417
|
+
},
|
|
418
|
+
"placeholder": {
|
|
419
|
+
"type": "string",
|
|
420
|
+
"default": "Pick a date",
|
|
421
|
+
"description": "Trigger text when nothing is selected"
|
|
422
|
+
},
|
|
423
|
+
"showTime": {
|
|
424
|
+
"type": "boolean",
|
|
425
|
+
"default": false,
|
|
426
|
+
"description": "Adds an HH:MM time picker (single mode)"
|
|
427
|
+
},
|
|
428
|
+
"minDate": {
|
|
429
|
+
"type": "Date",
|
|
430
|
+
"description": "Earliest selectable date"
|
|
431
|
+
},
|
|
432
|
+
"maxDate": {
|
|
433
|
+
"type": "Date",
|
|
434
|
+
"description": "Latest selectable date"
|
|
435
|
+
},
|
|
436
|
+
"disabled": {
|
|
437
|
+
"type": "function",
|
|
438
|
+
"description": "(date: Date) => boolean — disable specific dates"
|
|
439
|
+
},
|
|
440
|
+
"weekStartsOn": {
|
|
441
|
+
"type": "enum",
|
|
442
|
+
"values": [
|
|
443
|
+
0,
|
|
444
|
+
1
|
|
445
|
+
],
|
|
446
|
+
"default": 1,
|
|
447
|
+
"description": "0 = Sunday, 1 = Monday"
|
|
448
|
+
},
|
|
449
|
+
"inputClassName": {
|
|
450
|
+
"type": "string",
|
|
451
|
+
"description": "Class on the trigger button"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"antiPatterns": [
|
|
455
|
+
{
|
|
456
|
+
"pattern": "DatePicker mode=\"range\" for { from, to } object",
|
|
457
|
+
"reason": "DatePicker range mode emits a [Date, Date] tuple, not a { from, to } object",
|
|
458
|
+
"fix": "Use <DateRangePicker /> for the { from, to } object API + presets + dual month"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"pattern": "DatePicker without value/onChange",
|
|
462
|
+
"reason": "DatePicker is controlled — it has no internal selected state",
|
|
463
|
+
"fix": "Provide value and onChange (e.g. from useState)"
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
"examples": [
|
|
467
|
+
{
|
|
468
|
+
"description": "Single date field",
|
|
469
|
+
"code": "const [date, setDate] = useState(null);\n<DatePicker value={date} onChange={setDate} placeholder=\"Pick a date\" />"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"description": "Date + time",
|
|
473
|
+
"code": "<DatePicker value={date} onChange={setDate} showTime />"
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
},
|
|
377
477
|
"Card": {
|
|
378
478
|
"description": "Content container with optional header, body, and footer sections.",
|
|
379
479
|
"import": "import { Card, CardHeader, CardBody, CardFooter } from \"@usevyre/react\"",
|
|
@@ -464,6 +564,150 @@
|
|
|
464
564
|
}
|
|
465
565
|
]
|
|
466
566
|
},
|
|
567
|
+
"RadioGroup": {
|
|
568
|
+
"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.",
|
|
569
|
+
"import": "import { RadioGroup, Radio } from \"@usevyre/react\"",
|
|
570
|
+
"subcomponents": [
|
|
571
|
+
"Radio"
|
|
572
|
+
],
|
|
573
|
+
"props": {
|
|
574
|
+
"value": {
|
|
575
|
+
"type": "string",
|
|
576
|
+
"description": "Selected value (controlled). Vue: v-model."
|
|
577
|
+
},
|
|
578
|
+
"defaultValue": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"description": "Initial value (uncontrolled, React only)"
|
|
581
|
+
},
|
|
582
|
+
"onChange": {
|
|
583
|
+
"type": "function",
|
|
584
|
+
"description": "(value: string) => void. Vue: update:modelValue / v-model."
|
|
585
|
+
},
|
|
586
|
+
"name": {
|
|
587
|
+
"type": "string",
|
|
588
|
+
"description": "Radio input name; auto-generated if omitted"
|
|
589
|
+
},
|
|
590
|
+
"disabled": {
|
|
591
|
+
"type": "boolean",
|
|
592
|
+
"default": false,
|
|
593
|
+
"description": "Disable the whole group"
|
|
594
|
+
},
|
|
595
|
+
"size": {
|
|
596
|
+
"type": "enum",
|
|
597
|
+
"values": [
|
|
598
|
+
"sm",
|
|
599
|
+
"md"
|
|
600
|
+
],
|
|
601
|
+
"default": "md",
|
|
602
|
+
"description": "Control size"
|
|
603
|
+
},
|
|
604
|
+
"orientation": {
|
|
605
|
+
"type": "enum",
|
|
606
|
+
"values": [
|
|
607
|
+
"vertical",
|
|
608
|
+
"horizontal"
|
|
609
|
+
],
|
|
610
|
+
"default": "vertical",
|
|
611
|
+
"description": "Layout direction"
|
|
612
|
+
},
|
|
613
|
+
"options": {
|
|
614
|
+
"type": "{ value: string; label?: string; description?: string; disabled?: boolean }[]",
|
|
615
|
+
"description": "Data-driven options. Omit to pass <Radio> children instead."
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
"antiPatterns": [
|
|
619
|
+
{
|
|
620
|
+
"pattern": "<Radio> used outside a <RadioGroup>",
|
|
621
|
+
"reason": "Radio reads selection + name from RadioGroup context; it throws without a provider",
|
|
622
|
+
"fix": "Always wrap <Radio> in <RadioGroup>"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
"pattern": "RadioGroup without value/onChange (React) or v-model (Vue)",
|
|
626
|
+
"reason": "RadioGroup is controlled; selection won't update",
|
|
627
|
+
"fix": "Bind value + onChange (React) or v-model (Vue); or defaultValue for uncontrolled in React"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"pattern": "Using Checkbox for mutually-exclusive choices",
|
|
631
|
+
"reason": "Radio expresses single-choice semantics + keyboard model",
|
|
632
|
+
"fix": "Use RadioGroup + Radio (or options) for one-of-many"
|
|
633
|
+
}
|
|
634
|
+
],
|
|
635
|
+
"examples": [
|
|
636
|
+
{
|
|
637
|
+
"description": "Data-driven",
|
|
638
|
+
"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/>"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
"description": "Composable children",
|
|
642
|
+
"code": "<RadioGroup value={plan} onChange={setPlan} orientation=\"horizontal\">\n <Radio value=\"free\" label=\"Free\" />\n <Radio value=\"pro\" label=\"Pro\" />\n</RadioGroup>"
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
},
|
|
646
|
+
"RichTextEditor": {
|
|
647
|
+
"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.",
|
|
648
|
+
"import": "import { RichTextEditor } from \"@usevyre/react\"",
|
|
649
|
+
"props": {
|
|
650
|
+
"value": {
|
|
651
|
+
"type": "string",
|
|
652
|
+
"description": "HTML content (controlled). React: value + onChange. Vue: v-model."
|
|
653
|
+
},
|
|
654
|
+
"onChange": {
|
|
655
|
+
"type": "function",
|
|
656
|
+
"description": "(html: string) => void. Vue: update:modelValue / v-model."
|
|
657
|
+
},
|
|
658
|
+
"placeholder": {
|
|
659
|
+
"type": "string",
|
|
660
|
+
"default": "Write something…",
|
|
661
|
+
"description": "Shown when the editor is empty"
|
|
662
|
+
},
|
|
663
|
+
"disabled": {
|
|
664
|
+
"type": "boolean",
|
|
665
|
+
"default": false,
|
|
666
|
+
"description": "Not editable, dimmed; toolbar disabled"
|
|
667
|
+
},
|
|
668
|
+
"readOnly": {
|
|
669
|
+
"type": "boolean",
|
|
670
|
+
"default": false,
|
|
671
|
+
"description": "Not editable; toolbar hidden entirely"
|
|
672
|
+
},
|
|
673
|
+
"toolbar": {
|
|
674
|
+
"type": "RichTextTool[]",
|
|
675
|
+
"description": "Which buttons to show. Default = all. Tools: \"bold\"|\"italic\"|\"underline\"|\"strike\"|\"h1\"|\"h2\"|\"h3\"|\"ul\"|\"ol\"|\"quote\"|\"code\"|\"link\"|\"clear\""
|
|
676
|
+
},
|
|
677
|
+
"minHeight": {
|
|
678
|
+
"type": "string",
|
|
679
|
+
"default": "10rem",
|
|
680
|
+
"description": "CSS min-height of the editable area"
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
"antiPatterns": [
|
|
684
|
+
{
|
|
685
|
+
"pattern": "RichTextEditor without value/onChange (React) or v-model (Vue)",
|
|
686
|
+
"reason": "It is fully controlled; edits won't persist",
|
|
687
|
+
"fix": "Keep the HTML string in state and update it in onChange / v-model"
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"pattern": "Rendering value as text or with dangerouslySetInnerHTML elsewhere without sanitising",
|
|
691
|
+
"reason": "value is raw HTML the user typed",
|
|
692
|
+
"fix": "Sanitise (e.g. DOMPurify) before re-rendering untrusted RTE output"
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
"pattern": "toolbar=\"bold\" (string)",
|
|
696
|
+
"reason": "toolbar is an array of tool ids",
|
|
697
|
+
"fix": "Pass an array, e.g. toolbar={[\"bold\",\"italic\",\"link\"]}"
|
|
698
|
+
}
|
|
699
|
+
],
|
|
700
|
+
"examples": [
|
|
701
|
+
{
|
|
702
|
+
"description": "Controlled editor",
|
|
703
|
+
"code": "const [html, setHtml] = useState(\"<p>Hello <strong>world</strong></p>\");\n<RichTextEditor value={html} onChange={setHtml} placeholder=\"Write…\" />"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"description": "Minimal toolbar",
|
|
707
|
+
"code": "<RichTextEditor\n value={html}\n onChange={setHtml}\n toolbar={[\"bold\", \"italic\", \"link\"]}\n/>"
|
|
708
|
+
}
|
|
709
|
+
]
|
|
710
|
+
},
|
|
467
711
|
"Command": {
|
|
468
712
|
"description": "Command palette / search dialog. Use for search-first navigation or quick actions.",
|
|
469
713
|
"import": "import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandDialog } from \"@usevyre/react\"",
|
|
@@ -533,7 +777,7 @@
|
|
|
533
777
|
]
|
|
534
778
|
},
|
|
535
779
|
"Field": {
|
|
536
|
-
"description": "Form field wrapper
|
|
780
|
+
"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.",
|
|
537
781
|
"import": "import { Field, Input, Textarea } from \"@usevyre/react\"",
|
|
538
782
|
"props": {
|
|
539
783
|
"label": {
|
|
@@ -566,6 +810,11 @@
|
|
|
566
810
|
"pattern": "Applying state prop directly to Input",
|
|
567
811
|
"reason": "state belongs on Field, not on the Input itself",
|
|
568
812
|
"fix": "Wrap Input in <Field state=\"error\"> to apply validation styling"
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"pattern": "Mixing props label/hint AND FieldLabel/FieldError for the same field",
|
|
816
|
+
"reason": "Duplicates the label / message",
|
|
817
|
+
"fix": "Pick one: either props-based (label/hint/state) OR composable parts"
|
|
569
818
|
}
|
|
570
819
|
],
|
|
571
820
|
"examples": [
|
|
@@ -576,13 +825,28 @@
|
|
|
576
825
|
{
|
|
577
826
|
"description": "Search field with left icon",
|
|
578
827
|
"code": "<Field label=\"Search\">\n <Input leftElement={<SearchIcon />} placeholder=\"Search...\" />\n</Field>"
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"description": "Composable field with explicit parts",
|
|
831
|
+
"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>"
|
|
579
832
|
}
|
|
833
|
+
],
|
|
834
|
+
"subcomponents": [
|
|
835
|
+
"FieldLabel",
|
|
836
|
+
"FieldDescription",
|
|
837
|
+
"FieldError",
|
|
838
|
+
"FieldGroup",
|
|
839
|
+
"FieldSet"
|
|
580
840
|
]
|
|
581
841
|
},
|
|
582
842
|
"Input": {
|
|
583
843
|
"description": "Text input field. Wrap in Field for labels and validation. Use leftElement/rightElement for icons.",
|
|
584
844
|
"import": "import { Input } from \"@usevyre/react\"",
|
|
585
845
|
"props": {
|
|
846
|
+
"modelValue": {
|
|
847
|
+
"type": "string | number",
|
|
848
|
+
"description": "Vue only: enables v-model (two-way binding). React: use native value + onChange. Native input attrs (type, placeholder, disabled…) work in both."
|
|
849
|
+
},
|
|
586
850
|
"size": {
|
|
587
851
|
"type": "enum",
|
|
588
852
|
"values": [
|
|
@@ -612,6 +876,11 @@
|
|
|
612
876
|
"pattern": "type=\"search\" for search UI",
|
|
613
877
|
"reason": "Use Command component for search-first interfaces",
|
|
614
878
|
"fix": "Import Command from @usevyre/react for search palettes"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
"pattern": "Vue: binding Input/Textarea value without v-model",
|
|
882
|
+
"reason": "Vue Input & Textarea support v-model (modelValue); manual :value alone won't update",
|
|
883
|
+
"fix": "Use v-model on <Input>/<Textarea> in Vue; in React use value + onChange"
|
|
615
884
|
}
|
|
616
885
|
],
|
|
617
886
|
"examples": [
|
|
@@ -1278,6 +1547,656 @@
|
|
|
1278
1547
|
"code": "<Heading size=\"2xl\" as=\"h1\">Dashboard</Heading>\n<Lead>Welcome back. Here's what's happening today.</Lead>\n<Text size=\"sm\" style={{ color: 'var(--vyre-color-semantic-text-muted)' }}>Last updated 5 minutes ago.</Text>"
|
|
1279
1548
|
}
|
|
1280
1549
|
]
|
|
1550
|
+
},
|
|
1551
|
+
"ButtonGroup": {
|
|
1552
|
+
"description": "Groups multiple Button components into one visual unit (toolbar, segmented control). Pure layout — no internal state.",
|
|
1553
|
+
"import": "import { ButtonGroup, Button } from \"@usevyre/react\"",
|
|
1554
|
+
"props": {
|
|
1555
|
+
"orientation": {
|
|
1556
|
+
"type": "enum",
|
|
1557
|
+
"values": [
|
|
1558
|
+
"horizontal",
|
|
1559
|
+
"vertical"
|
|
1560
|
+
],
|
|
1561
|
+
"default": "horizontal",
|
|
1562
|
+
"description": "Layout direction of grouped buttons"
|
|
1563
|
+
},
|
|
1564
|
+
"attached": {
|
|
1565
|
+
"type": "boolean",
|
|
1566
|
+
"default": false,
|
|
1567
|
+
"description": "Remove gap so buttons share collapsed borders (segmented control look)"
|
|
1568
|
+
},
|
|
1569
|
+
"size": {
|
|
1570
|
+
"type": "enum",
|
|
1571
|
+
"values": [
|
|
1572
|
+
"sm",
|
|
1573
|
+
"md",
|
|
1574
|
+
"lg",
|
|
1575
|
+
"icon"
|
|
1576
|
+
],
|
|
1577
|
+
"description": "Size forwarded to all child buttons"
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
"antiPatterns": [
|
|
1581
|
+
{
|
|
1582
|
+
"pattern": "ButtonGroup variant=\"...\"",
|
|
1583
|
+
"reason": "ButtonGroup has no variant prop — variant goes on each child Button",
|
|
1584
|
+
"fix": "Set variant on each <Button> inside the group"
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
"pattern": "ButtonGroup without Button children",
|
|
1588
|
+
"reason": "ButtonGroup is a layout wrapper for Button elements",
|
|
1589
|
+
"fix": "Place <Button> elements as direct children"
|
|
1590
|
+
}
|
|
1591
|
+
],
|
|
1592
|
+
"examples": [
|
|
1593
|
+
{
|
|
1594
|
+
"description": "Segmented control",
|
|
1595
|
+
"code": "<ButtonGroup attached>\n <Button variant=\"secondary\">Day</Button>\n <Button variant=\"secondary\">Week</Button>\n <Button variant=\"secondary\">Month</Button>\n</ButtonGroup>"
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
"description": "Vertical group",
|
|
1599
|
+
"code": "<ButtonGroup orientation=\"vertical\" attached>\n <Button variant=\"secondary\">Top</Button>\n <Button variant=\"secondary\">Bottom</Button>\n</ButtonGroup>"
|
|
1600
|
+
}
|
|
1601
|
+
]
|
|
1602
|
+
},
|
|
1603
|
+
"TagsInput": {
|
|
1604
|
+
"description": "Multi-tag input. Type and press Enter or comma to add a tag, click x to remove, Backspace on empty input removes the last tag. Controlled.",
|
|
1605
|
+
"import": "import { TagsInput } from \"@usevyre/react\"",
|
|
1606
|
+
"props": {
|
|
1607
|
+
"value": {
|
|
1608
|
+
"type": "string[]",
|
|
1609
|
+
"description": "Controlled list of tags (required)"
|
|
1610
|
+
},
|
|
1611
|
+
"onChange": {
|
|
1612
|
+
"type": "(tags: string[]) => void",
|
|
1613
|
+
"description": "Called with the new tag array on every change. React only — Vue uses v-model"
|
|
1614
|
+
},
|
|
1615
|
+
"placeholder": {
|
|
1616
|
+
"type": "string",
|
|
1617
|
+
"description": "Input placeholder, hidden when max is reached"
|
|
1618
|
+
},
|
|
1619
|
+
"disabled": {
|
|
1620
|
+
"type": "boolean",
|
|
1621
|
+
"default": false,
|
|
1622
|
+
"description": "Disables all interaction"
|
|
1623
|
+
},
|
|
1624
|
+
"max": {
|
|
1625
|
+
"type": "number",
|
|
1626
|
+
"description": "Maximum number of tags; input is disabled when reached"
|
|
1627
|
+
},
|
|
1628
|
+
"size": {
|
|
1629
|
+
"type": "enum",
|
|
1630
|
+
"values": [
|
|
1631
|
+
"sm",
|
|
1632
|
+
"md",
|
|
1633
|
+
"lg"
|
|
1634
|
+
],
|
|
1635
|
+
"default": "md",
|
|
1636
|
+
"description": "Size scale"
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
"antiPatterns": [
|
|
1640
|
+
{
|
|
1641
|
+
"pattern": "TagsInput value={string}",
|
|
1642
|
+
"reason": "value must be a string array, not a string",
|
|
1643
|
+
"fix": "Pass an array: value={['react','vue']}"
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
"pattern": "TagsInput without onChange",
|
|
1647
|
+
"reason": "TagsInput is controlled — it needs onChange to update",
|
|
1648
|
+
"fix": "Provide value and onChange (React) or v-model (Vue)"
|
|
1649
|
+
}
|
|
1650
|
+
],
|
|
1651
|
+
"examples": [
|
|
1652
|
+
{
|
|
1653
|
+
"description": "Basic tags input",
|
|
1654
|
+
"code": "const [tags, setTags] = useState<string[]>([]);\n<TagsInput value={tags} onChange={setTags} placeholder=\"Add a tag…\" />"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
"description": "Limited to 5 tags",
|
|
1658
|
+
"code": "<TagsInput value={tags} onChange={setTags} max={5} />"
|
|
1659
|
+
}
|
|
1660
|
+
]
|
|
1661
|
+
},
|
|
1662
|
+
"Combobox": {
|
|
1663
|
+
"description": "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).",
|
|
1664
|
+
"import": "import { Combobox } from \"@usevyre/react\"",
|
|
1665
|
+
"props": {
|
|
1666
|
+
"options": {
|
|
1667
|
+
"type": "{ value: string; label: string; disabled?: boolean }[]",
|
|
1668
|
+
"description": "Selectable options (required)"
|
|
1669
|
+
},
|
|
1670
|
+
"value": {
|
|
1671
|
+
"type": "string | null",
|
|
1672
|
+
"description": "Controlled selected value; null clears selection (required)"
|
|
1673
|
+
},
|
|
1674
|
+
"onChange": {
|
|
1675
|
+
"type": "(value: string | null) => void",
|
|
1676
|
+
"description": "Called when selection changes. React only — Vue uses v-model"
|
|
1677
|
+
},
|
|
1678
|
+
"placeholder": {
|
|
1679
|
+
"type": "string",
|
|
1680
|
+
"default": "\"Search…\"",
|
|
1681
|
+
"description": "Input placeholder when nothing is selected"
|
|
1682
|
+
},
|
|
1683
|
+
"disabled": {
|
|
1684
|
+
"type": "boolean",
|
|
1685
|
+
"default": false,
|
|
1686
|
+
"description": "Disables the combobox"
|
|
1687
|
+
},
|
|
1688
|
+
"size": {
|
|
1689
|
+
"type": "enum",
|
|
1690
|
+
"values": [
|
|
1691
|
+
"sm",
|
|
1692
|
+
"md",
|
|
1693
|
+
"lg"
|
|
1694
|
+
],
|
|
1695
|
+
"default": "md",
|
|
1696
|
+
"description": "Height scale"
|
|
1697
|
+
},
|
|
1698
|
+
"emptyText": {
|
|
1699
|
+
"type": "string",
|
|
1700
|
+
"default": "\"No results\"",
|
|
1701
|
+
"description": "Text shown when search matches no options"
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
"antiPatterns": [
|
|
1705
|
+
{
|
|
1706
|
+
"pattern": "Combobox value=\"\"",
|
|
1707
|
+
"reason": "Empty selection must be null, not empty string",
|
|
1708
|
+
"fix": "Use value={null} for no selection"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"pattern": "Combobox options={string[]}",
|
|
1712
|
+
"reason": "options must be objects with value and label",
|
|
1713
|
+
"fix": "Use [{ value: 'ts', label: 'TypeScript' }]"
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
"pattern": "Using Combobox for command palette",
|
|
1717
|
+
"reason": "Combobox is single-value select, not an action palette",
|
|
1718
|
+
"fix": "Use Command for command palettes"
|
|
1719
|
+
}
|
|
1720
|
+
],
|
|
1721
|
+
"examples": [
|
|
1722
|
+
{
|
|
1723
|
+
"description": "Searchable language picker",
|
|
1724
|
+
"code": "const [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/>"
|
|
1725
|
+
}
|
|
1726
|
+
]
|
|
1727
|
+
},
|
|
1728
|
+
"DataGrid": {
|
|
1729
|
+
"description": "Table with built-in column sorting, loading skeletons, and empty state. Filtering and pagination are out of scope — compose with the Pagination component.",
|
|
1730
|
+
"import": "import { DataGrid } from \"@usevyre/react\"",
|
|
1731
|
+
"props": {
|
|
1732
|
+
"columns": {
|
|
1733
|
+
"type": "{ key: string; label: string; sortable?: boolean; width?: string }[]",
|
|
1734
|
+
"description": "Column definitions (required)"
|
|
1735
|
+
},
|
|
1736
|
+
"rows": {
|
|
1737
|
+
"type": "Record<string, unknown>[]",
|
|
1738
|
+
"description": "Row data; each object keyed by column key (required)"
|
|
1739
|
+
},
|
|
1740
|
+
"sortKey": {
|
|
1741
|
+
"type": "string",
|
|
1742
|
+
"description": "Controlled active sort column key"
|
|
1743
|
+
},
|
|
1744
|
+
"sortDir": {
|
|
1745
|
+
"type": "enum",
|
|
1746
|
+
"values": [
|
|
1747
|
+
"asc",
|
|
1748
|
+
"desc"
|
|
1749
|
+
],
|
|
1750
|
+
"description": "Controlled sort direction"
|
|
1751
|
+
},
|
|
1752
|
+
"onSort": {
|
|
1753
|
+
"type": "(key: string, dir: 'asc' | 'desc') => void",
|
|
1754
|
+
"description": "Called when a sortable header is clicked. React only — Vue emits sort + v-model:sortKey/sortDir"
|
|
1755
|
+
},
|
|
1756
|
+
"loading": {
|
|
1757
|
+
"type": "boolean",
|
|
1758
|
+
"default": false,
|
|
1759
|
+
"description": "Show skeleton placeholder rows"
|
|
1760
|
+
},
|
|
1761
|
+
"emptyText": {
|
|
1762
|
+
"type": "string",
|
|
1763
|
+
"default": "\"No data\"",
|
|
1764
|
+
"description": "Message shown when rows is empty"
|
|
1765
|
+
},
|
|
1766
|
+
"stickyHeader": {
|
|
1767
|
+
"type": "boolean",
|
|
1768
|
+
"default": false,
|
|
1769
|
+
"description": "Keep the header visible while scrolling"
|
|
1770
|
+
}
|
|
1771
|
+
},
|
|
1772
|
+
"antiPatterns": [
|
|
1773
|
+
{
|
|
1774
|
+
"pattern": "DataGrid expecting built-in pagination",
|
|
1775
|
+
"reason": "DataGrid does not paginate — it only sorts",
|
|
1776
|
+
"fix": "Slice rows yourself and use the Pagination component"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
"pattern": "DataGrid expecting built-in filtering",
|
|
1780
|
+
"reason": "DataGrid does not filter rows",
|
|
1781
|
+
"fix": "Filter the rows array before passing it in"
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"pattern": "sortable without onSort",
|
|
1785
|
+
"reason": "Sorting is controlled — the grid does not sort data itself",
|
|
1786
|
+
"fix": "Handle onSort and sort the rows array in your state"
|
|
1787
|
+
}
|
|
1788
|
+
],
|
|
1789
|
+
"examples": [
|
|
1790
|
+
{
|
|
1791
|
+
"description": "Sortable grid",
|
|
1792
|
+
"code": "const 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/>"
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
"description": "Loading state",
|
|
1796
|
+
"code": "<DataGrid columns={cols} rows={[]} loading />"
|
|
1797
|
+
}
|
|
1798
|
+
]
|
|
1799
|
+
},
|
|
1800
|
+
"Tag": {
|
|
1801
|
+
"description": "Standalone display tag/chip for categories, labels, or filter chips. NOT an input — for tag input use TagsInput. Group multiple with TagGroup.",
|
|
1802
|
+
"import": "import { Tag } from \"@usevyre/react\"",
|
|
1803
|
+
"props": {
|
|
1804
|
+
"variant": {
|
|
1805
|
+
"type": "enum",
|
|
1806
|
+
"values": [
|
|
1807
|
+
"default",
|
|
1808
|
+
"accent",
|
|
1809
|
+
"danger"
|
|
1810
|
+
],
|
|
1811
|
+
"default": "default",
|
|
1812
|
+
"description": "Visual style. default=neutral, accent=brand, danger=destructive/error"
|
|
1813
|
+
},
|
|
1814
|
+
"size": {
|
|
1815
|
+
"type": "enum",
|
|
1816
|
+
"values": [
|
|
1817
|
+
"sm",
|
|
1818
|
+
"md",
|
|
1819
|
+
"lg"
|
|
1820
|
+
],
|
|
1821
|
+
"default": "md",
|
|
1822
|
+
"description": "Size scale"
|
|
1823
|
+
},
|
|
1824
|
+
"onRemove": {
|
|
1825
|
+
"type": "() => void",
|
|
1826
|
+
"description": "React only. When provided, renders a × remove button. Vue: use `removable` prop + @remove event"
|
|
1827
|
+
},
|
|
1828
|
+
"onClick": {
|
|
1829
|
+
"type": "() => void",
|
|
1830
|
+
"description": "React only. When provided, makes the whole tag interactive (keyboard accessible). Vue: use `clickable` prop + @click event"
|
|
1831
|
+
},
|
|
1832
|
+
"disabled": {
|
|
1833
|
+
"type": "boolean",
|
|
1834
|
+
"default": false,
|
|
1835
|
+
"description": "Disables interaction (only relevant with onClick/onRemove)"
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
"antiPatterns": [
|
|
1839
|
+
{
|
|
1840
|
+
"pattern": "Tag variant=\"success\"",
|
|
1841
|
+
"reason": "Tag only has default, accent, danger variants (unlike Badge which has more)",
|
|
1842
|
+
"fix": "Use Badge for success/warning/teal status colors; Tag is for categories/filters"
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
"pattern": "Using Tag for tag input",
|
|
1846
|
+
"reason": "Tag is display-only, it does not accept typed input",
|
|
1847
|
+
"fix": "Use TagsInput for adding/removing tags via keyboard"
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
"pattern": "Tag size=\"xl\"",
|
|
1851
|
+
"reason": "Maximum size is 'lg'",
|
|
1852
|
+
"fix": "Use size=\"lg\""
|
|
1853
|
+
}
|
|
1854
|
+
],
|
|
1855
|
+
"examples": [
|
|
1856
|
+
{
|
|
1857
|
+
"description": "Category tags in a group",
|
|
1858
|
+
"code": "<TagGroup>\n <Tag>Design</Tag>\n <Tag variant=\"accent\">Featured</Tag>\n <Tag>Engineering</Tag>\n</TagGroup>"
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
"description": "Removable filter chip (React)",
|
|
1862
|
+
"code": "<Tag onRemove={() => removeFilter(\"react\")}>react</Tag>"
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
"description": "Clickable toggle tag (React)",
|
|
1866
|
+
"code": "<Tag onClick={() => toggleFilter(\"vue\")}>vue</Tag>"
|
|
1867
|
+
}
|
|
1868
|
+
]
|
|
1869
|
+
},
|
|
1870
|
+
"TagGroup": {
|
|
1871
|
+
"description": "Read-only container that lays out multiple Tag elements with automatic wrapping and consistent spacing. For tag input use TagsInput.",
|
|
1872
|
+
"import": "import { TagGroup, Tag } from \"@usevyre/react\"",
|
|
1873
|
+
"props": {
|
|
1874
|
+
"gap": {
|
|
1875
|
+
"type": "enum",
|
|
1876
|
+
"values": [
|
|
1877
|
+
"sm",
|
|
1878
|
+
"md",
|
|
1879
|
+
"lg"
|
|
1880
|
+
],
|
|
1881
|
+
"default": "md",
|
|
1882
|
+
"description": "Spacing between tags"
|
|
1883
|
+
},
|
|
1884
|
+
"wrap": {
|
|
1885
|
+
"type": "boolean",
|
|
1886
|
+
"default": true,
|
|
1887
|
+
"description": "Wrap tags onto multiple lines when they overflow; set false for single-line scroll"
|
|
1888
|
+
}
|
|
1889
|
+
},
|
|
1890
|
+
"antiPatterns": [
|
|
1891
|
+
{
|
|
1892
|
+
"pattern": "TagGroup without Tag children",
|
|
1893
|
+
"reason": "TagGroup is a layout wrapper for Tag elements",
|
|
1894
|
+
"fix": "Place <Tag> elements as direct children"
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
"pattern": "Using TagGroup for tag input",
|
|
1898
|
+
"reason": "TagGroup is display-only",
|
|
1899
|
+
"fix": "Use TagsInput for an editable tag field"
|
|
1900
|
+
}
|
|
1901
|
+
],
|
|
1902
|
+
"examples": [
|
|
1903
|
+
{
|
|
1904
|
+
"description": "Tag group with mixed variants",
|
|
1905
|
+
"code": "<TagGroup gap=\"sm\">\n <Tag>React</Tag>\n <Tag>Vue</Tag>\n <Tag variant=\"accent\">TypeScript</Tag>\n</TagGroup>"
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1908
|
+
},
|
|
1909
|
+
"Item": {
|
|
1910
|
+
"description": "Layout primitive for list rows, settings rows, and notification rows. Denser than Card — use Item (not Card) for repeated list rows.",
|
|
1911
|
+
"import": "import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemGroup } from \"@usevyre/react\"",
|
|
1912
|
+
"subcomponents": [
|
|
1913
|
+
"ItemGroup",
|
|
1914
|
+
"ItemMedia",
|
|
1915
|
+
"ItemContent",
|
|
1916
|
+
"ItemTitle",
|
|
1917
|
+
"ItemDescription",
|
|
1918
|
+
"ItemActions"
|
|
1919
|
+
],
|
|
1920
|
+
"props": {
|
|
1921
|
+
"variant": {
|
|
1922
|
+
"type": "enum",
|
|
1923
|
+
"values": [
|
|
1924
|
+
"default",
|
|
1925
|
+
"outlined",
|
|
1926
|
+
"muted",
|
|
1927
|
+
"plain"
|
|
1928
|
+
],
|
|
1929
|
+
"default": "default",
|
|
1930
|
+
"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)."
|
|
1931
|
+
},
|
|
1932
|
+
"size": {
|
|
1933
|
+
"type": "enum",
|
|
1934
|
+
"values": [
|
|
1935
|
+
"sm",
|
|
1936
|
+
"md",
|
|
1937
|
+
"lg"
|
|
1938
|
+
],
|
|
1939
|
+
"default": "md",
|
|
1940
|
+
"description": "Vertical density / text size of the row"
|
|
1941
|
+
},
|
|
1942
|
+
"clickable": {
|
|
1943
|
+
"type": "boolean",
|
|
1944
|
+
"default": false,
|
|
1945
|
+
"description": "Adds pointer cursor, hover background, focus ring, and role=button"
|
|
1946
|
+
}
|
|
1947
|
+
},
|
|
1948
|
+
"antiPatterns": [
|
|
1949
|
+
{
|
|
1950
|
+
"pattern": "Card used for repeated list rows",
|
|
1951
|
+
"reason": "Card is a content container; Item is the dense list-row primitive",
|
|
1952
|
+
"fix": "Use <Item> (optionally inside <ItemGroup separated>) for list/settings rows"
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
"pattern": "Item variant=\"primary\"",
|
|
1956
|
+
"reason": "Item has no 'primary' variant",
|
|
1957
|
+
"fix": "Use variant=\"default\" | \"outlined\" | \"muted\""
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
"pattern": "raw text directly inside Item",
|
|
1961
|
+
"reason": "Item lays out media/content/actions columns; bare text breaks alignment",
|
|
1962
|
+
"fix": "Wrap text in <ItemContent><ItemTitle>…</ItemTitle></ItemContent>"
|
|
1963
|
+
}
|
|
1964
|
+
],
|
|
1965
|
+
"examples": [
|
|
1966
|
+
{
|
|
1967
|
+
"description": "Settings row with media, content and a trailing switch",
|
|
1968
|
+
"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>"
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
"description": "Grouped clickable list with dividers",
|
|
1972
|
+
"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>"
|
|
1973
|
+
}
|
|
1974
|
+
]
|
|
1975
|
+
},
|
|
1976
|
+
"Kanban": {
|
|
1977
|
+
"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.",
|
|
1978
|
+
"import": "import { Kanban } from \"@usevyre/react\"",
|
|
1979
|
+
"props": {
|
|
1980
|
+
"value": {
|
|
1981
|
+
"type": "KanbanColumn[]",
|
|
1982
|
+
"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."
|
|
1983
|
+
},
|
|
1984
|
+
"onChange": {
|
|
1985
|
+
"type": "function",
|
|
1986
|
+
"description": "(next: KanbanColumn[]) => void — called with the next columns array after a drag move. Required; set it back into your state."
|
|
1987
|
+
},
|
|
1988
|
+
"renderCard": {
|
|
1989
|
+
"type": "function",
|
|
1990
|
+
"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."
|
|
1991
|
+
},
|
|
1992
|
+
"onCardClick": {
|
|
1993
|
+
"type": "function",
|
|
1994
|
+
"description": "(card, column) => void — fired on click/Enter/Space (not after a drag). Vue: @card-click."
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
"antiPatterns": [
|
|
1998
|
+
{
|
|
1999
|
+
"pattern": "Kanban without onChange (or ignoring it)",
|
|
2000
|
+
"reason": "Kanban holds no internal data state; dropped cards won't move unless you apply onChange",
|
|
2001
|
+
"fix": "Store columns in state and setColumns in onChange (v-model in Vue)"
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
"pattern": "Duplicate card ids across columns",
|
|
2005
|
+
"reason": "Card moves are resolved by id; duplicates corrupt the move",
|
|
2006
|
+
"fix": "Use globally-unique card ids across the entire board"
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
"pattern": "Mutating value in place then calling onChange",
|
|
2010
|
+
"reason": "React/Vue need a new array reference to re-render reliably",
|
|
2011
|
+
"fix": "Pass the new array Kanban gives you straight to setState / v-model"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"pattern": "color=\"blue\" (or any non-semantic value)",
|
|
2015
|
+
"reason": "color is a fixed semantic set, not an arbitrary CSS color",
|
|
2016
|
+
"fix": "Use one of: \"default\" | \"accent\" | \"teal\" | \"success\" | \"warning\" | \"danger\""
|
|
2017
|
+
}
|
|
2018
|
+
],
|
|
2019
|
+
"examples": [
|
|
2020
|
+
{
|
|
2021
|
+
"description": "Controlled board",
|
|
2022
|
+
"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} />"
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
"description": "Custom card body + click handler",
|
|
2026
|
+
"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/>"
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
"description": "Tinted columns/cards + complex card content",
|
|
2030
|
+
"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/>"
|
|
2031
|
+
}
|
|
2032
|
+
]
|
|
2033
|
+
},
|
|
2034
|
+
"Conversation": {
|
|
2035
|
+
"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.",
|
|
2036
|
+
"import": "import { Conversation } from \"@usevyre/react\"",
|
|
2037
|
+
"props": {
|
|
2038
|
+
"value": {
|
|
2039
|
+
"type": "ConversationMessage[]",
|
|
2040
|
+
"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."
|
|
2041
|
+
},
|
|
2042
|
+
"currentUserId": {
|
|
2043
|
+
"type": "string",
|
|
2044
|
+
"description": "Whose messages are outgoing (aligned right). Required — match against message.authorId."
|
|
2045
|
+
},
|
|
2046
|
+
"composer": {
|
|
2047
|
+
"type": "boolean",
|
|
2048
|
+
"default": false,
|
|
2049
|
+
"description": "Show the built-in input + Send button. Pair with onSend."
|
|
2050
|
+
},
|
|
2051
|
+
"onSend": {
|
|
2052
|
+
"type": "function",
|
|
2053
|
+
"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."
|
|
2054
|
+
},
|
|
2055
|
+
"placeholder": {
|
|
2056
|
+
"type": "string",
|
|
2057
|
+
"default": "Write a message…",
|
|
2058
|
+
"description": "Composer input placeholder."
|
|
2059
|
+
},
|
|
2060
|
+
"typing": {
|
|
2061
|
+
"type": "boolean | string",
|
|
2062
|
+
"default": false,
|
|
2063
|
+
"description": "Show an incoming typing indicator. Pass a string to label it."
|
|
2064
|
+
},
|
|
2065
|
+
"allowAttachments": {
|
|
2066
|
+
"type": "boolean",
|
|
2067
|
+
"default": false,
|
|
2068
|
+
"description": "Show a 📎 attach button + staged-file chips in the built-in composer. Files come back via onSend's second arg."
|
|
2069
|
+
},
|
|
2070
|
+
"accept": {
|
|
2071
|
+
"type": "string",
|
|
2072
|
+
"description": "Forwarded to the file input's accept attribute, e.g. \"image/*\"."
|
|
2073
|
+
},
|
|
2074
|
+
"renderMessage": {
|
|
2075
|
+
"type": "function",
|
|
2076
|
+
"description": "(message, meta) => ReactNode — custom bubble body. meta = { outgoing, isGroupStart, isGroupEnd }. Vue: #message scoped slot."
|
|
2077
|
+
},
|
|
2078
|
+
"renderComposer": {
|
|
2079
|
+
"type": "function",
|
|
2080
|
+
"description": "(api) => ReactNode — replace the composer entirely. api = { value, setValue, files, setFiles, send }. Vue: #composer scoped slot."
|
|
2081
|
+
}
|
|
2082
|
+
},
|
|
2083
|
+
"antiPatterns": [
|
|
2084
|
+
{
|
|
2085
|
+
"pattern": "Conversation without currentUserId",
|
|
2086
|
+
"reason": "Alignment (incoming vs outgoing) is decided by authorId === currentUserId",
|
|
2087
|
+
"fix": "Always pass currentUserId matching one of the message authorId values"
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
"pattern": "Expecting Conversation to store/append messages",
|
|
2091
|
+
"reason": "Conversation is controlled & stateless for data, like Kanban/DataGrid",
|
|
2092
|
+
"fix": "Append to your own state in onSend (or @send) and pass it back via value"
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
"pattern": "composer without onSend (React) / @send (Vue)",
|
|
2096
|
+
"reason": "The built-in composer emits text; nothing happens unless you handle it",
|
|
2097
|
+
"fix": "Provide onSend / @send to append the message to value"
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
"pattern": "Treating onSend as (text) only when using allowAttachments",
|
|
2101
|
+
"reason": "With allowAttachments, staged files arrive as the 2nd arg; ignoring it drops attachments",
|
|
2102
|
+
"fix": "Handle onSend(text, files) — map files to message attachments and append"
|
|
2103
|
+
}
|
|
2104
|
+
],
|
|
2105
|
+
"examples": [
|
|
2106
|
+
{
|
|
2107
|
+
"description": "Controlled thread with built-in composer",
|
|
2108
|
+
"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/>"
|
|
2109
|
+
},
|
|
2110
|
+
{
|
|
2111
|
+
"description": "Typing indicator + custom bubble",
|
|
2112
|
+
"code": "<Conversation\n value={messages}\n currentUserId=\"me\"\n typing=\"Sam is typing\"\n renderMessage={(m) => <strong>{m.text}</strong>}\n/>"
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
"description": "Message with image + file attachments",
|
|
2116
|
+
"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\" />"
|
|
2117
|
+
}
|
|
2118
|
+
]
|
|
2119
|
+
},
|
|
2120
|
+
"DateRangePicker": {
|
|
2121
|
+
"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.",
|
|
2122
|
+
"import": "import { DateRangePicker } from \"@usevyre/react\"",
|
|
2123
|
+
"props": {
|
|
2124
|
+
"value": {
|
|
2125
|
+
"type": "{ from: Date | null; to: Date | null } | null",
|
|
2126
|
+
"description": "Selected range (controlled). Pass an OBJECT, not a tuple."
|
|
2127
|
+
},
|
|
2128
|
+
"onChange": {
|
|
2129
|
+
"type": "function",
|
|
2130
|
+
"description": "Callback (range: { from, to }) => void when the range changes"
|
|
2131
|
+
},
|
|
2132
|
+
"placeholder": {
|
|
2133
|
+
"type": "string",
|
|
2134
|
+
"default": "Pick a date range",
|
|
2135
|
+
"description": "Trigger text when no range is selected"
|
|
2136
|
+
},
|
|
2137
|
+
"numberOfMonths": {
|
|
2138
|
+
"type": "enum",
|
|
2139
|
+
"values": [
|
|
2140
|
+
1,
|
|
2141
|
+
2
|
|
2142
|
+
],
|
|
2143
|
+
"default": 2,
|
|
2144
|
+
"description": "How many month calendars to show side-by-side"
|
|
2145
|
+
},
|
|
2146
|
+
"presets": {
|
|
2147
|
+
"type": "boolean | DateRangePreset[]",
|
|
2148
|
+
"default": false,
|
|
2149
|
+
"description": "true shows built-in presets (Today, Yesterday, Last 7/30 days, This/Last month); or pass a custom array of { label, range() }"
|
|
2150
|
+
},
|
|
2151
|
+
"minDate": {
|
|
2152
|
+
"type": "Date",
|
|
2153
|
+
"description": "Earliest selectable date"
|
|
2154
|
+
},
|
|
2155
|
+
"maxDate": {
|
|
2156
|
+
"type": "Date",
|
|
2157
|
+
"description": "Latest selectable date"
|
|
2158
|
+
},
|
|
2159
|
+
"disabled": {
|
|
2160
|
+
"type": "function",
|
|
2161
|
+
"description": "(date: Date) => boolean — disable specific dates"
|
|
2162
|
+
},
|
|
2163
|
+
"weekStartsOn": {
|
|
2164
|
+
"type": "enum",
|
|
2165
|
+
"values": [
|
|
2166
|
+
0,
|
|
2167
|
+
1
|
|
2168
|
+
],
|
|
2169
|
+
"default": 1,
|
|
2170
|
+
"description": "0 = Sunday, 1 = Monday"
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2173
|
+
"antiPatterns": [
|
|
2174
|
+
{
|
|
2175
|
+
"pattern": "value={[from, to]}",
|
|
2176
|
+
"reason": "DateRangePicker uses a { from, to } object, not a [Date, Date] tuple like Calendar mode=range",
|
|
2177
|
+
"fix": "Use value={{ from, to }} and read range.from / range.to"
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
"pattern": "DateRangePicker for a single date",
|
|
2181
|
+
"reason": "DateRangePicker always selects a start AND end",
|
|
2182
|
+
"fix": "Use <DatePicker /> for a single date"
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
"pattern": "presets=\"true\" (string)",
|
|
2186
|
+
"reason": "presets is a boolean or an array, not a string",
|
|
2187
|
+
"fix": "Use the bare prop: presets (or presets={true})"
|
|
2188
|
+
}
|
|
2189
|
+
],
|
|
2190
|
+
"examples": [
|
|
2191
|
+
{
|
|
2192
|
+
"description": "Range picker with built-in presets",
|
|
2193
|
+
"code": "const [range, setRange] = useState({ from: null, to: null });\n<DateRangePicker value={range} onChange={setRange} presets />"
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
"description": "Single month, no presets",
|
|
2197
|
+
"code": "<DateRangePicker value={range} onChange={setRange} numberOfMonths={1} />"
|
|
2198
|
+
}
|
|
2199
|
+
]
|
|
1281
2200
|
}
|
|
1282
2201
|
}
|
|
1283
2202
|
}
|