astralyx-ui 0.3.1 → 0.6.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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/registry/index.json +453 -1
  3. package/registry/items/ab-test-results.json +21 -0
  4. package/registry/items/attribution.json +19 -0
  5. package/registry/items/back-to-top.json +21 -0
  6. package/registry/items/bottom-nav.json +19 -0
  7. package/registry/items/box-plot.json +19 -0
  8. package/registry/items/cascader.json +23 -0
  9. package/registry/items/cookie-consent.json +21 -0
  10. package/registry/items/currency-input.json +19 -0
  11. package/registry/items/emoji-picker.json +22 -0
  12. package/registry/items/event-stream.json +23 -0
  13. package/registry/items/gantt.json +19 -0
  14. package/registry/items/image-cropper.json +20 -0
  15. package/registry/items/knowledge-graph.json +1 -1
  16. package/registry/items/lib-qr.json +14 -0
  17. package/registry/items/lib-sql-select.json +14 -0
  18. package/registry/items/lib-styles.json +1 -1
  19. package/registry/items/markdown-editor.json +23 -0
  20. package/registry/items/note-graph.json +1 -1
  21. package/registry/items/org-chart.json +21 -0
  22. package/registry/items/pdf-viewer.json +22 -0
  23. package/registry/items/phone-input.json +19 -0
  24. package/registry/items/popconfirm.json +24 -0
  25. package/registry/items/qr-code.json +20 -0
  26. package/registry/items/query-constructor.json +32 -0
  27. package/registry/items/radar-chart.json +19 -0
  28. package/registry/items/result.json +21 -0
  29. package/registry/items/retention-curve.json +19 -0
  30. package/registry/items/sankey.json +19 -0
  31. package/registry/items/scatter-plot.json +19 -0
  32. package/registry/items/scheduler.json +19 -0
  33. package/registry/items/segment-builder.json +25 -0
  34. package/registry/items/segmented-control.json +19 -0
  35. package/registry/items/select.json +1 -1
  36. package/registry/items/signature-pad.json +20 -0
  37. package/registry/items/transfer.json +24 -0
  38. package/registry/items/tree-select.json +25 -0
  39. package/registry/items/treemap.json +19 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astralyx-ui",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "309 accessible React components you copy into your repo, with a CLI and registry that resolve what each one needs.",
6
6
  "license": "MIT",
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "astralyx-ui",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
4
4
  "homepage": "https://ui.astralyx.dev",
5
5
  "items": [
6
+ {
7
+ "name": "ab-test-results",
8
+ "type": "registry:ui",
9
+ "title": "A/B Test Results",
10
+ "description": "Variants with lift, a confidence interval and a two-proportion z-test — plus the peeking warning, because stopping at the first significant reading is the failure that actually happens.",
11
+ "category": "Analytics",
12
+ "dependencies": [
13
+ "lucide-react@^1.39.0"
14
+ ],
15
+ "registryDependencies": [
16
+ "lib-styles",
17
+ "lib-utils"
18
+ ]
19
+ },
6
20
  {
7
21
  "name": "accordion",
8
22
  "type": "registry:ui",
@@ -216,6 +230,18 @@
216
230
  "progress"
217
231
  ]
218
232
  },
233
+ {
234
+ "name": "attribution",
235
+ "type": "registry:ui",
236
+ "title": "Attribution",
237
+ "description": "Credit for a conversion split across touchpoints, with the model as a control rather than a buried setting — because the disagreement between models is the whole finding.",
238
+ "category": "Analytics",
239
+ "dependencies": [],
240
+ "registryDependencies": [
241
+ "lib-styles",
242
+ "lib-utils"
243
+ ]
244
+ },
219
245
  {
220
246
  "name": "audio-player",
221
247
  "type": "registry:ui",
@@ -264,6 +290,20 @@
264
290
  "lib-utils"
265
291
  ]
266
292
  },
293
+ {
294
+ "name": "back-to-top",
295
+ "type": "registry:ui",
296
+ "title": "Back To Top",
297
+ "description": "Appears once you have scrolled, driven by an IntersectionObserver sentinel rather than a scroll handler — and it moves focus, not just the viewport.",
298
+ "category": "Navigation",
299
+ "dependencies": [
300
+ "lucide-react@^1.39.0"
301
+ ],
302
+ "registryDependencies": [
303
+ "lib-styles",
304
+ "lib-utils"
305
+ ]
306
+ },
267
307
  {
268
308
  "name": "backup-list",
269
309
  "type": "registry:ui",
@@ -355,6 +395,30 @@
355
395
  "tooltip"
356
396
  ]
357
397
  },
398
+ {
399
+ "name": "bottom-nav",
400
+ "type": "registry:ui",
401
+ "title": "Bottom Nav",
402
+ "description": "The mobile tab bar — three to five destinations, pinned to the bottom where thumbs are, with the home-indicator safe area accounted for.",
403
+ "category": "Navigation",
404
+ "dependencies": [],
405
+ "registryDependencies": [
406
+ "lib-styles",
407
+ "lib-utils"
408
+ ]
409
+ },
410
+ {
411
+ "name": "box-plot",
412
+ "type": "registry:ui",
413
+ "title": "Box Plot",
414
+ "description": "Quartiles, Tukey whiskers and outliers — the distribution an average hides. Quartiles use linear interpolation, so they agree with what your analytics stack reports.",
415
+ "category": "Charts",
416
+ "dependencies": [],
417
+ "registryDependencies": [
418
+ "lib-styles",
419
+ "lib-utils"
420
+ ]
421
+ },
358
422
  {
359
423
  "name": "branch-select",
360
424
  "type": "registry:ui",
@@ -622,6 +686,22 @@
622
686
  "lib-utils"
623
687
  ]
624
688
  },
689
+ {
690
+ "name": "cascader",
691
+ "type": "registry:ui",
692
+ "title": "Cascader",
693
+ "description": "A regular hierarchy picked one level at a time, in side-by-side columns — so the path stays readable and the siblings you passed are still visible.",
694
+ "category": "Forms",
695
+ "dependencies": [
696
+ "lucide-react@^1.39.0"
697
+ ],
698
+ "registryDependencies": [
699
+ "lib-styles",
700
+ "lib-utils",
701
+ "primitive-dismissable",
702
+ "primitive-popper"
703
+ ]
704
+ },
625
705
  {
626
706
  "name": "chain-select",
627
707
  "type": "registry:ui",
@@ -985,6 +1065,20 @@
985
1065
  "lib-utils"
986
1066
  ]
987
1067
  },
1068
+ {
1069
+ "name": "cookie-consent",
1070
+ "type": "registry:ui",
1071
+ "title": "Cookie Consent",
1072
+ "description": "A consent banner with reject as easy as accept, nothing pre-ticked, and no cookie written by the component itself — the arrangement regulators have repeatedly ruled is the compliant one.",
1073
+ "category": "Utility",
1074
+ "dependencies": [],
1075
+ "registryDependencies": [
1076
+ "button",
1077
+ "checkbox",
1078
+ "lib-styles",
1079
+ "lib-utils"
1080
+ ]
1081
+ },
988
1082
  {
989
1083
  "name": "coordinate-input",
990
1084
  "type": "registry:ui",
@@ -1096,6 +1190,18 @@
1096
1190
  "code-block"
1097
1191
  ]
1098
1192
  },
1193
+ {
1194
+ "name": "currency-input",
1195
+ "type": "registry:ui",
1196
+ "title": "Currency Input",
1197
+ "description": "A money field whose value is an integer number of minor units, formatted only while unfocused. Reads its decimal count from Intl, so zero-decimal currencies work without a special case.",
1198
+ "category": "Forms",
1199
+ "dependencies": [],
1200
+ "registryDependencies": [
1201
+ "lib-styles",
1202
+ "lib-utils"
1203
+ ]
1204
+ },
1099
1205
  {
1100
1206
  "name": "data-grid",
1101
1207
  "type": "registry:ui",
@@ -1345,6 +1451,21 @@
1345
1451
  "upload-list"
1346
1452
  ]
1347
1453
  },
1454
+ {
1455
+ "name": "emoji-picker",
1456
+ "type": "registry:ui",
1457
+ "title": "Emoji Picker",
1458
+ "description": "A grouped, searchable grid with recents, rendered as text rather than sprites. One tab stop with arrow-key movement, and it never touches localStorage.",
1459
+ "category": "Forms",
1460
+ "dependencies": [
1461
+ "lucide-react@^1.39.0"
1462
+ ],
1463
+ "registryDependencies": [
1464
+ "input",
1465
+ "lib-styles",
1466
+ "lib-utils"
1467
+ ]
1468
+ },
1348
1469
  {
1349
1470
  "name": "empty",
1350
1471
  "type": "registry:ui",
@@ -1451,6 +1572,22 @@
1451
1572
  "lib-utils"
1452
1573
  ]
1453
1574
  },
1575
+ {
1576
+ "name": "event-stream",
1577
+ "type": "registry:ui",
1578
+ "title": "Event Stream",
1579
+ "description": "A live tail of analytics events with their payloads — the tool for debugging the tracking itself. Autoscroll pauses when you scroll away and the list is capped rather than unbounded.",
1580
+ "category": "Analytics",
1581
+ "dependencies": [
1582
+ "lucide-react@^1.39.0"
1583
+ ],
1584
+ "registryDependencies": [
1585
+ "button",
1586
+ "input",
1587
+ "lib-styles",
1588
+ "lib-utils"
1589
+ ]
1590
+ },
1454
1591
  {
1455
1592
  "name": "feature-flag",
1456
1593
  "type": "registry:ui",
@@ -1625,6 +1762,18 @@
1625
1762
  "lib-utils"
1626
1763
  ]
1627
1764
  },
1765
+ {
1766
+ "name": "gantt",
1767
+ "type": "registry:ui",
1768
+ "title": "Gantt",
1769
+ "description": "Work on a timeline: drag a bar to reschedule and its edges to change the start or end, with a computed critical path and dependency conflicts surfaced as you create them.",
1770
+ "category": "Views",
1771
+ "dependencies": [],
1772
+ "registryDependencies": [
1773
+ "lib-styles",
1774
+ "lib-utils"
1775
+ ]
1776
+ },
1628
1777
  {
1629
1778
  "name": "gas-tracker",
1630
1779
  "type": "registry:ui",
@@ -1764,6 +1913,19 @@
1764
1913
  "lib-utils"
1765
1914
  ]
1766
1915
  },
1916
+ {
1917
+ "name": "image-cropper",
1918
+ "type": "registry:ui",
1919
+ "title": "Image Cropper",
1920
+ "description": "The step between picking an image and saving it. The crop is stored in natural image coordinates and exported with toBlob, so it survives a resize and uploads directly.",
1921
+ "category": "Media",
1922
+ "dependencies": [],
1923
+ "registryDependencies": [
1924
+ "button",
1925
+ "lib-styles",
1926
+ "lib-utils"
1927
+ ]
1928
+ },
1767
1929
  {
1768
1930
  "name": "image",
1769
1931
  "type": "registry:ui",
@@ -2143,6 +2305,22 @@
2143
2305
  "lib-utils"
2144
2306
  ]
2145
2307
  },
2308
+ {
2309
+ "name": "markdown-editor",
2310
+ "type": "registry:ui",
2311
+ "title": "Markdown Editor",
2312
+ "description": "A real textarea, a toolbar that edits the selection through setRangeText — so the browser undo stack survives — and the Markdown component as its preview.",
2313
+ "category": "Knowledge",
2314
+ "dependencies": [
2315
+ "lucide-react@^1.39.0"
2316
+ ],
2317
+ "registryDependencies": [
2318
+ "button",
2319
+ "lib-styles",
2320
+ "lib-utils",
2321
+ "markdown"
2322
+ ]
2323
+ },
2146
2324
  {
2147
2325
  "name": "markdown",
2148
2326
  "type": "registry:ui",
@@ -2712,6 +2890,20 @@
2712
2890
  "lib-utils"
2713
2891
  ]
2714
2892
  },
2893
+ {
2894
+ "name": "org-chart",
2895
+ "type": "registry:ui",
2896
+ "title": "Org Chart",
2897
+ "description": "A reporting hierarchy drawn top-down with connectors, built from flat { id, managerId } rows. Orphans and multiple roots are rendered rather than dropped; cycles are reported.",
2898
+ "category": "Views",
2899
+ "dependencies": [
2900
+ "lucide-react@^1.39.0"
2901
+ ],
2902
+ "registryDependencies": [
2903
+ "lib-styles",
2904
+ "lib-utils"
2905
+ ]
2906
+ },
2715
2907
  {
2716
2908
  "name": "page-header",
2717
2909
  "type": "registry:ui",
@@ -2784,6 +2976,21 @@
2784
2976
  "stepper"
2785
2977
  ]
2786
2978
  },
2979
+ {
2980
+ "name": "pdf-viewer",
2981
+ "type": "registry:ui",
2982
+ "title": "PDF Viewer",
2983
+ "description": "A PDF in the browser’s own viewer with a toolbar around it. It delegates the rendering on purpose, and always shows a download link — because embedded PDFs fail routinely.",
2984
+ "category": "Media",
2985
+ "dependencies": [
2986
+ "lucide-react@^1.39.0"
2987
+ ],
2988
+ "registryDependencies": [
2989
+ "button",
2990
+ "lib-styles",
2991
+ "lib-utils"
2992
+ ]
2993
+ },
2787
2994
  {
2788
2995
  "name": "permission-matrix",
2789
2996
  "type": "registry:ui",
@@ -2799,6 +3006,18 @@
2799
3006
  "lib-utils"
2800
3007
  ]
2801
3008
  },
3009
+ {
3010
+ "name": "phone-input",
3011
+ "type": "registry:ui",
3012
+ "title": "Phone Input",
3013
+ "description": "A country selector and a national number that together produce E.164 — the shape every telephony API wants. Honest about not being a validator.",
3014
+ "category": "Forms",
3015
+ "dependencies": [],
3016
+ "registryDependencies": [
3017
+ "lib-styles",
3018
+ "lib-utils"
3019
+ ]
3020
+ },
2802
3021
  {
2803
3022
  "name": "pipeline",
2804
3023
  "type": "registry:ui",
@@ -2815,6 +3034,23 @@
2815
3034
  "spinner"
2816
3035
  ]
2817
3036
  },
3037
+ {
3038
+ "name": "popconfirm",
3039
+ "type": "registry:ui",
3040
+ "title": "Popconfirm",
3041
+ "description": "A confirmation anchored to the control that triggered it, for reversible or single-item actions — so the row you are asking about stays visible.",
3042
+ "category": "Overlays",
3043
+ "dependencies": [
3044
+ "lucide-react@^1.39.0"
3045
+ ],
3046
+ "registryDependencies": [
3047
+ "button",
3048
+ "lib-styles",
3049
+ "lib-utils",
3050
+ "primitive-dismissable",
3051
+ "primitive-popper"
3052
+ ]
3053
+ },
2818
3054
  {
2819
3055
  "name": "popover",
2820
3056
  "type": "registry:ui",
@@ -3034,6 +3270,44 @@
3034
3270
  "lib-utils"
3035
3271
  ]
3036
3272
  },
3273
+ {
3274
+ "name": "qr-code",
3275
+ "type": "registry:ui",
3276
+ "title": "QR Code",
3277
+ "description": "A QR code encoded in the browser and drawn as SVG. No dependency and no network round trip — which matters, because the two things people encode most are a TOTP secret and a wallet address.",
3278
+ "category": "Utility",
3279
+ "dependencies": [],
3280
+ "registryDependencies": [
3281
+ "lib-qr",
3282
+ "lib-styles",
3283
+ "lib-utils"
3284
+ ]
3285
+ },
3286
+ {
3287
+ "name": "query-constructor",
3288
+ "type": "registry:ui",
3289
+ "title": "Query Constructor",
3290
+ "description": "A SELECT assembled a clause at a time — sources with a join button, columns, filter, then order, group and limit — with the finished statement underneath. Reads both ways: paste a SELECT and the blocks fill in. Values compile to bound parameters and identifiers are allow-listed against the schema.",
3291
+ "category": "Database",
3292
+ "dependencies": [
3293
+ "lucide-react@^1.39.0"
3294
+ ],
3295
+ "registryDependencies": [
3296
+ "button",
3297
+ "code-block",
3298
+ "command",
3299
+ "input",
3300
+ "lib-sql-select",
3301
+ "lib-styles",
3302
+ "lib-utils",
3303
+ "multi-select",
3304
+ "popover",
3305
+ "schema-table",
3306
+ "segment-builder",
3307
+ "select",
3308
+ "textarea"
3309
+ ]
3310
+ },
3037
3311
  {
3038
3312
  "name": "query-editor",
3039
3313
  "type": "registry:ui",
@@ -3083,6 +3357,18 @@
3083
3357
  "sparkline"
3084
3358
  ]
3085
3359
  },
3360
+ {
3361
+ "name": "radar-chart",
3362
+ "type": "registry:ui",
3363
+ "title": "Radar Chart",
3364
+ "description": "Several measures for one subject on axes radiating from a centre. Good for recognising a profile’s shape; documented plainly as the wrong chart when the reader needs to compare values.",
3365
+ "category": "Charts",
3366
+ "dependencies": [],
3367
+ "registryDependencies": [
3368
+ "lib-styles",
3369
+ "lib-utils"
3370
+ ]
3371
+ },
3086
3372
  {
3087
3373
  "name": "radio-group",
3088
3374
  "type": "registry:ui",
@@ -3268,6 +3554,32 @@
3268
3554
  "lib-utils"
3269
3555
  ]
3270
3556
  },
3557
+ {
3558
+ "name": "result",
3559
+ "type": "registry:ui",
3560
+ "title": "Result",
3561
+ "description": "The whole-page outcome — not found, forbidden, server error, done. Announced on render, so a client-side route change to an error page is not silent for a screen reader.",
3562
+ "category": "Feedback",
3563
+ "dependencies": [
3564
+ "lucide-react@^1.39.0"
3565
+ ],
3566
+ "registryDependencies": [
3567
+ "lib-styles",
3568
+ "lib-utils"
3569
+ ]
3570
+ },
3571
+ {
3572
+ "name": "retention-curve",
3573
+ "type": "registry:ui",
3574
+ "title": "Retention Curve",
3575
+ "description": "What fraction of each cohort is still here, period by period — normalised so growth cannot hide churn. Later cohorts are drawn shorter, not padded with zeroes.",
3576
+ "category": "Analytics",
3577
+ "dependencies": [],
3578
+ "registryDependencies": [
3579
+ "lib-styles",
3580
+ "lib-utils"
3581
+ ]
3582
+ },
3271
3583
  {
3272
3584
  "name": "retrieval-results",
3273
3585
  "type": "registry:ui",
@@ -3431,6 +3743,42 @@
3431
3743
  "switch"
3432
3744
  ]
3433
3745
  },
3746
+ {
3747
+ "name": "sankey",
3748
+ "type": "registry:ui",
3749
+ "title": "Sankey",
3750
+ "description": "Flow between stages, where band width is the quantity moving. Nodes are layered by longest path so every link runs forward, and a cycle is reported rather than drawn as something plausible.",
3751
+ "category": "Charts",
3752
+ "dependencies": [],
3753
+ "registryDependencies": [
3754
+ "lib-styles",
3755
+ "lib-utils"
3756
+ ]
3757
+ },
3758
+ {
3759
+ "name": "scatter-plot",
3760
+ "type": "registry:ui",
3761
+ "title": "Scatter Plot",
3762
+ "description": "Two measured variables plotted against each other, with an optional least-squares trend line and Pearson’s r. Points can carry a third dimension as area and a fourth as colour.",
3763
+ "category": "Charts",
3764
+ "dependencies": [],
3765
+ "registryDependencies": [
3766
+ "lib-styles",
3767
+ "lib-utils"
3768
+ ]
3769
+ },
3770
+ {
3771
+ "name": "scheduler",
3772
+ "type": "registry:ui",
3773
+ "title": "Scheduler",
3774
+ "description": "A week of days with events laid out against the hours and edited in place — drag to move across days, drag an edge to change the end, drag empty grid to create. Overlaps split into columns rather than stacking.",
3775
+ "category": "Views",
3776
+ "dependencies": [],
3777
+ "registryDependencies": [
3778
+ "lib-styles",
3779
+ "lib-utils"
3780
+ ]
3781
+ },
3434
3782
  {
3435
3783
  "name": "schema-form",
3436
3784
  "type": "registry:ui",
@@ -3507,6 +3855,36 @@
3507
3855
  "lib-utils"
3508
3856
  ]
3509
3857
  },
3858
+ {
3859
+ "name": "segment-builder",
3860
+ "type": "registry:ui",
3861
+ "title": "Segment Builder",
3862
+ "description": "Build an audience from conditions, with precedence shown as nesting rather than implied by a word — and the output a structure, not a concatenated string.",
3863
+ "category": "Analytics",
3864
+ "dependencies": [
3865
+ "lucide-react@^1.39.0"
3866
+ ],
3867
+ "registryDependencies": [
3868
+ "button",
3869
+ "input",
3870
+ "lib-styles",
3871
+ "lib-utils",
3872
+ "select",
3873
+ "tag-input"
3874
+ ]
3875
+ },
3876
+ {
3877
+ "name": "segmented-control",
3878
+ "type": "registry:ui",
3879
+ "title": "Segmented Control",
3880
+ "description": "A small set of mutually exclusive choices shown at once, built from real radio inputs in a fieldset — so arrow keys, form submission and screen-reader announcement all work without any script.",
3881
+ "category": "Forms",
3882
+ "dependencies": [],
3883
+ "registryDependencies": [
3884
+ "lib-styles",
3885
+ "lib-utils"
3886
+ ]
3887
+ },
3510
3888
  {
3511
3889
  "name": "select",
3512
3890
  "type": "registry:ui",
@@ -3612,6 +3990,19 @@
3612
3990
  "tooltip"
3613
3991
  ]
3614
3992
  },
3993
+ {
3994
+ "name": "signature-pad",
3995
+ "type": "registry:ui",
3996
+ "title": "Signature Pad",
3997
+ "description": "A canvas you sign with a finger, stylus or mouse. Strokes are kept as points and redrawn, so undo, resize and export all work — and stylus pressure varies the line.",
3998
+ "category": "Forms",
3999
+ "dependencies": [],
4000
+ "registryDependencies": [
4001
+ "button",
4002
+ "lib-styles",
4003
+ "lib-utils"
4004
+ ]
4005
+ },
3615
4006
  {
3616
4007
  "name": "skeleton",
3617
4008
  "type": "registry:ui",
@@ -4436,6 +4827,41 @@
4436
4827
  "wallet-address"
4437
4828
  ]
4438
4829
  },
4830
+ {
4831
+ "name": "transfer",
4832
+ "type": "registry:ui",
4833
+ "title": "Transfer",
4834
+ "description": "Two lists and the movement between them, for choosing a subset from a large pool. Both sides are searchable — filtering the target is what makes a 200-item selection usable.",
4835
+ "category": "Forms",
4836
+ "dependencies": [
4837
+ "lucide-react@^1.39.0"
4838
+ ],
4839
+ "registryDependencies": [
4840
+ "button",
4841
+ "checkbox",
4842
+ "input",
4843
+ "lib-styles",
4844
+ "lib-utils"
4845
+ ]
4846
+ },
4847
+ {
4848
+ "name": "tree-select",
4849
+ "type": "registry:ui",
4850
+ "title": "Tree Select",
4851
+ "description": "A tree in a popover with real tri-state parents. Checking a branch checks its subtree, the value holds only leaves, and searching keeps the ancestors of every match.",
4852
+ "category": "Forms",
4853
+ "dependencies": [
4854
+ "lucide-react@^1.39.0"
4855
+ ],
4856
+ "registryDependencies": [
4857
+ "checkbox",
4858
+ "input",
4859
+ "lib-styles",
4860
+ "lib-utils",
4861
+ "primitive-dismissable",
4862
+ "primitive-popper"
4863
+ ]
4864
+ },
4439
4865
  {
4440
4866
  "name": "tree",
4441
4867
  "type": "registry:ui",
@@ -4450,6 +4876,18 @@
4450
4876
  "lib-utils"
4451
4877
  ]
4452
4878
  },
4879
+ {
4880
+ "name": "treemap",
4881
+ "type": "registry:ui",
4882
+ "title": "Treemap",
4883
+ "description": "Part-to-whole as nested rectangles sized by area, laid out with the squarify algorithm so tiles stay close to square instead of degenerating into slivers.",
4884
+ "category": "Charts",
4885
+ "dependencies": [],
4886
+ "registryDependencies": [
4887
+ "lib-styles",
4888
+ "lib-utils"
4889
+ ]
4890
+ },
4453
4891
  {
4454
4892
  "name": "two-factor-setup",
4455
4893
  "type": "registry:ui",
@@ -4790,6 +5228,20 @@
4790
5228
  ],
4791
5229
  "registryDependencies": []
4792
5230
  },
5231
+ {
5232
+ "name": "lib-qr",
5233
+ "type": "registry:lib",
5234
+ "title": "qr",
5235
+ "dependencies": [],
5236
+ "registryDependencies": []
5237
+ },
5238
+ {
5239
+ "name": "lib-sql-select",
5240
+ "type": "registry:lib",
5241
+ "title": "sql-select",
5242
+ "dependencies": [],
5243
+ "registryDependencies": []
5244
+ },
4793
5245
  {
4794
5246
  "name": "lib-styles",
4795
5247
  "type": "registry:lib",
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "ab-test-results",
3
+ "type": "registry:ui",
4
+ "title": "A/B Test Results",
5
+ "description": "Variants with lift, a confidence interval and a two-proportion z-test — plus the peeking warning, because stopping at the first significant reading is the failure that actually happens.",
6
+ "category": "Analytics",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/ab-test-results.tsx",
17
+ "type": "registry:ui",
18
+ "content": "import { useId, useMemo, type ComponentProps, type ReactNode } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Experiment variants with lift, a confidence interval, and an honest verdict.\n *\n * **It shows the interval, not just the number.** \"+12% lift\" is not a result;\n * \"+12%, 95% CI [−3%, +27%]\" is, and it says the opposite — that the experiment\n * has not concluded. A dashboard that prints the point estimate alone\n * manufactures certainty, and people ship on it.\n *\n * **The maths.** A two-proportion z-test with a pooled standard error for the\n * p-value, and an unpooled standard error for the interval — the standard pair,\n * and the reason the interval can straddle zero while p sits just under 0.05.\n * The interval is then divided by the control rate so it is on the same scale\n * as the lift beside it; an absolute-difference interval printed next to a\n * relative lift is read as bounding that lift, and it does not.\n * The normal approximation needs roughly ten conversions in each arm, so below\n * that the verdict is withheld rather than computed on data too thin to carry\n * it.\n *\n * **It warns about peeking, because that is the real-world failure.** Checking\n * an experiment repeatedly and stopping at the first significant reading\n * inflates the false-positive rate far above the nominal 5% — a fixed-horizon\n * test read continuously is wrong roughly a third of the time. If a sample-size\n * target is given and has not been reached, this says so plainly.\n *\n * It reports a *statistical* result about one metric. Whether that metric is\n * the one that matters, whether the assignment was actually random, and whether\n * the effect is worth the change are not things any component can tell you.\n */\nexport type Variant = {\n id: string\n name: ReactNode\n /** Users in this arm. */\n visitors: number\n conversions: number\n /** Exactly one variant should be the baseline. */\n control?: boolean\n}\n\ntype AbTestResultsProps = Omit<ComponentProps<'div'>, 'title'> & {\n variants: Variant[]\n title?: ReactNode\n /** Two-sided significance threshold. */\n alpha?: number\n /** Per-arm sample size the test was planned for. Enables the peeking warning. */\n targetSample?: number\n metricLabel?: string\n emptyLabel?: string\n label?: string\n}\n\n/** Normal CDF via Abramowitz & Stegun 7.1.26 — accurate to ~1e-7. */\nfunction normalCdf(z: number): number {\n const sign = z < 0 ? -1 : 1\n const x = Math.abs(z) / Math.SQRT2\n const t = 1 / (1 + 0.3275911 * x)\n const y =\n 1 -\n ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *\n t *\n Math.exp(-x * x)\n return 0.5 * (1 + sign * y)\n}\n\n/** 95% ≈ 1.96. Inverse normal by bisection: exact enough, and tiny. */\nfunction zFor(confidence: number): number {\n let lo = 0\n let hi = 6\n const target = 1 - (1 - confidence) / 2\n for (let i = 0; i < 60; i++) {\n const mid = (lo + hi) / 2\n if (normalCdf(mid) < target) lo = mid\n else hi = mid\n }\n return (lo + hi) / 2\n}\n\nfunction AbTestResults({\n variants,\n title,\n alpha = 0.05,\n targetSample,\n metricLabel = 'Conversion',\n emptyLabel = 'No variants.',\n label = 'Experiment results',\n className,\n ...props\n}: AbTestResultsProps) {\n const titleId = useId()\n\n const rows = useMemo(() => {\n const control = variants.find((variant) => variant.control) ?? variants[0]\n if (!control) return []\n\n const z = zFor(1 - alpha)\n const controlRate = control.visitors > 0 ? control.conversions / control.visitors : 0\n\n return variants.map((variant) => {\n const rate = variant.visitors > 0 ? variant.conversions / variant.visitors : 0\n if (variant.id === control.id) {\n return { variant, rate, isControl: true, lift: null, ci: null, p: null, thin: false }\n }\n\n // The normal approximation needs enough successes and failures in both\n // arms; under that, no verdict is honest.\n const thin =\n Math.min(\n variant.conversions,\n variant.visitors - variant.conversions,\n control.conversions,\n control.visitors - control.conversions,\n ) < 10\n\n const pooled =\n (variant.conversions + control.conversions) / (variant.visitors + control.visitors)\n const pooledSe = Math.sqrt(\n pooled * (1 - pooled) * (1 / variant.visitors + 1 / control.visitors),\n )\n const statistic = pooledSe > 0 ? (rate - controlRate) / pooledSe : 0\n const p = 2 * (1 - normalCdf(Math.abs(statistic)))\n\n // Unpooled for the interval — pooling assumes the null it is testing.\n const se = Math.sqrt(\n (rate * (1 - rate)) / variant.visitors + (controlRate * (1 - controlRate)) / control.visitors,\n )\n const diff = rate - controlRate\n\n /**\n * The interval is put on the same scale as the lift beside it.\n *\n * The z-test gives an interval for the *absolute* difference in rates —\n * 0.75 percentage points. Printing that next to a *relative* lift of\n * +12.5% invites the reader to take it as bounding the lift, which it\n * does not: the two numbers are in different units and disagree by a\n * factor of the control rate. Dividing by the control rate is the\n * first-order (delta method) interval for relative lift, which is what\n * every experiment dashboard means by \"95% CI\" on a lift column.\n */\n const ci: [number, number] =\n controlRate > 0\n ? [(diff - z * se) / controlRate, (diff + z * se) / controlRate]\n : [diff - z * se, diff + z * se]\n\n return {\n variant,\n rate,\n isControl: false,\n lift: controlRate > 0 ? diff / controlRate : null,\n ci,\n p,\n thin,\n }\n })\n }, [variants, alpha])\n\n if (rows.length === 0) {\n return (\n <div className={cn(surface, radius.surface, 'p-4', className)} {...props}>\n <p className=\"text-muted-foreground text-xs\">{emptyLabel}</p>\n </div>\n )\n }\n\n const underpowered =\n targetSample !== undefined && variants.some((variant) => variant.visitors < targetSample)\n\n const percent = (value: number) => `${(value * 100).toFixed(2)}%`\n const signed = (value: number) => `${value >= 0 ? '+' : ''}${(value * 100).toFixed(1)}%`\n\n return (\n <div\n data-slot=\"ab-test-results\"\n className={cn(surface, radius.surface, 'overflow-hidden', className)}\n aria-labelledby={titleId}\n {...props}\n >\n {title && (\n <div className=\"border-border border-b px-4 py-3\">\n <p id={titleId} className=\"text-sm font-medium\">\n {title}\n </p>\n </div>\n )}\n {!title && (\n <p id={titleId} className=\"sr-only\">\n {label}\n </p>\n )}\n\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-start text-sm\">\n <thead>\n <tr className=\"border-border text-muted-foreground border-b text-xs\">\n <th className=\"px-4 py-2 text-start font-medium\">Variant</th>\n <th className=\"px-4 py-2 text-end font-medium\">Visitors</th>\n <th className=\"px-4 py-2 text-end font-medium\">{metricLabel}</th>\n <th className=\"px-4 py-2 text-end font-medium\">Lift</th>\n <th className=\"px-4 py-2 text-end font-medium\">\n {Math.round((1 - alpha) * 100)}% CI on lift\n </th>\n <th className=\"px-4 py-2 text-end font-medium\">p</th>\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => {\n const significant = row.p !== null && !row.thin && row.p < alpha\n return (\n <tr key={row.variant.id} className=\"border-border/60 border-b last:border-b-0\">\n <td className=\"px-4 py-2\">\n <span className=\"flex items-center gap-2\">\n {row.variant.name}\n {row.isControl && (\n <span className=\"text-muted-foreground text-[11px]\">control</span>\n )}\n </span>\n </td>\n <td className=\"px-4 py-2 text-end tabular-nums\">\n {row.variant.visitors.toLocaleString()}\n </td>\n <td className=\"px-4 py-2 text-end tabular-nums\">\n {percent(row.rate)}\n <span className=\"text-muted-foreground ms-1 text-[11px]\">\n ({row.variant.conversions})\n </span>\n </td>\n <td\n className={cn(\n 'px-4 py-2 text-end tabular-nums',\n significant && (row.lift ?? 0) > 0 && 'text-[var(--green-soft-foreground)]',\n significant && (row.lift ?? 0) < 0 && 'text-[var(--destructive)]',\n )}\n >\n {row.lift === null ? '—' : signed(row.lift)}\n </td>\n <td className=\"text-muted-foreground px-4 py-2 text-end text-xs tabular-nums\">\n {row.ci ? `[${signed(row.ci[0])}, ${signed(row.ci[1])}]` : '—'}\n </td>\n <td className=\"px-4 py-2 text-end tabular-nums\">\n {row.p === null ? (\n '—'\n ) : row.thin ? (\n <span className=\"text-muted-foreground text-xs\">too few</span>\n ) : (\n <span className={cn(significant && 'font-medium')}>\n {row.p < 0.001 ? '<0.001' : row.p.toFixed(3)}\n </span>\n )}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n\n {/* The warning that matters more than the p-value. */}\n {underpowered && (\n <p\n role=\"status\"\n className=\"border-border text-muted-foreground flex items-start gap-2 border-t px-4 py-2 text-xs\"\n >\n <AlertTriangle aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0\" />\n <span>\n Below the planned {targetSample?.toLocaleString()} per arm. Stopping at the first\n significant reading inflates the false-positive rate well past {Math.round(alpha * 100)}%\n — treat anything here as provisional.\n </span>\n </p>\n )}\n </div>\n )\n}\n\nexport { AbTestResults }\nexport type { AbTestResultsProps }\n"
19
+ }
20
+ ]
21
+ }