@astryx-vue/cli 0.4.1 → 0.4.3
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/CONTRIBUTING.md +53 -0
- package/README.md +10 -162
- package/README.zh-CN.md +10 -161
- package/dist/index.js +2196 -384
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -633,7 +633,7 @@ const catalog = {
|
|
|
633
633
|
{
|
|
634
634
|
"name": "@astryx-vue/core",
|
|
635
635
|
"directory": "packages/core",
|
|
636
|
-
"version": "0.
|
|
636
|
+
"version": "0.6.0",
|
|
637
637
|
"description": "Vue 3 components for the unofficial astryx-vue port of Astryx. Not affiliated with Meta.",
|
|
638
638
|
"canary": false,
|
|
639
639
|
"published": true
|
|
@@ -641,23 +641,23 @@ const catalog = {
|
|
|
641
641
|
{
|
|
642
642
|
"name": "@astryx-vue/charts",
|
|
643
643
|
"directory": "packages/charts",
|
|
644
|
-
"version": "0.
|
|
645
|
-
"description": "Astryx charts for Vue — a config-model data visualization layer (d3-based).
|
|
646
|
-
"canary":
|
|
647
|
-
"published":
|
|
644
|
+
"version": "0.1.1",
|
|
645
|
+
"description": "Astryx charts for Vue — a config-model data visualization layer (d3-based).",
|
|
646
|
+
"canary": false,
|
|
647
|
+
"published": true
|
|
648
648
|
},
|
|
649
649
|
{
|
|
650
650
|
"name": "@astryx-vue/lab",
|
|
651
651
|
"directory": "packages/lab",
|
|
652
|
-
"version": "0.
|
|
652
|
+
"version": "0.2.0",
|
|
653
653
|
"description": "Astryx lab components for Vue 3 — experimental components that may change. Ships only under the canary dist-tag.",
|
|
654
654
|
"canary": true,
|
|
655
|
-
"published":
|
|
655
|
+
"published": true
|
|
656
656
|
},
|
|
657
657
|
{
|
|
658
658
|
"name": "@astryx-vue/richtext",
|
|
659
659
|
"directory": "packages/richtext",
|
|
660
|
-
"version": "0.
|
|
660
|
+
"version": "0.6.0",
|
|
661
661
|
"description": "Astryx rich text for Vue — Lexical nodes and lossless Markdown import/export. Not affiliated with Meta.",
|
|
662
662
|
"canary": false,
|
|
663
663
|
"published": true
|
|
@@ -665,7 +665,7 @@ const catalog = {
|
|
|
665
665
|
{
|
|
666
666
|
"name": "@astryx-vue/themes",
|
|
667
667
|
"directory": "packages/themes",
|
|
668
|
-
"version": "0.
|
|
668
|
+
"version": "0.6.0",
|
|
669
669
|
"description": "CSS themes for the unofficial astryx-vue port of Astryx. Not affiliated with Meta.",
|
|
670
670
|
"canary": false,
|
|
671
671
|
"published": true
|
|
@@ -19383,6 +19383,1504 @@ const catalog = {
|
|
|
19383
19383
|
"zh": "https://astryx-vue.pages.dev/components/icon"
|
|
19384
19384
|
}
|
|
19385
19385
|
},
|
|
19386
|
+
{
|
|
19387
|
+
"slug": "chart",
|
|
19388
|
+
"name": "Chart",
|
|
19389
|
+
"package": "@astryx-vue/charts",
|
|
19390
|
+
"kind": "component",
|
|
19391
|
+
"title": {
|
|
19392
|
+
"en": "Chart",
|
|
19393
|
+
"zh": "图表"
|
|
19394
|
+
},
|
|
19395
|
+
"group": "Data Visualization",
|
|
19396
|
+
"groupTitle": {
|
|
19397
|
+
"en": "Data Visualization",
|
|
19398
|
+
"zh": "数据可视化"
|
|
19399
|
+
},
|
|
19400
|
+
"importPath": "@astryx-vue/charts",
|
|
19401
|
+
"exportName": "Chart",
|
|
19402
|
+
"summary": {
|
|
19403
|
+
"en": "Line chart with revenue and cost series, grid lines, axes, a bottom legend, and hover tooltips.",
|
|
19404
|
+
"zh": "包含收入与成本两条折线,带有网格、坐标轴、底部图例以及鼠标悬停提示框。"
|
|
19405
|
+
},
|
|
19406
|
+
"usage": {
|
|
19407
|
+
"en": "",
|
|
19408
|
+
"zh": ""
|
|
19409
|
+
},
|
|
19410
|
+
"practices": {
|
|
19411
|
+
"en": [],
|
|
19412
|
+
"zh": []
|
|
19413
|
+
},
|
|
19414
|
+
"props": {
|
|
19415
|
+
"en": [
|
|
19416
|
+
{
|
|
19417
|
+
"heading": "Line (`line`)",
|
|
19418
|
+
"rows": [
|
|
19419
|
+
{
|
|
19420
|
+
"name": "`color`",
|
|
19421
|
+
"description": "`color`",
|
|
19422
|
+
"type": "`string`",
|
|
19423
|
+
"default": "Auto from theme palette"
|
|
19424
|
+
},
|
|
19425
|
+
{
|
|
19426
|
+
"name": "`curve`",
|
|
19427
|
+
"description": "`curve`",
|
|
19428
|
+
"type": "`'linear' \\",
|
|
19429
|
+
"default": "'monotone' \\"
|
|
19430
|
+
},
|
|
19431
|
+
{
|
|
19432
|
+
"name": "`strokeWidth`",
|
|
19433
|
+
"description": "`strokeWidth`",
|
|
19434
|
+
"type": "`number`",
|
|
19435
|
+
"default": "`2`"
|
|
19436
|
+
},
|
|
19437
|
+
{
|
|
19438
|
+
"name": "`dots`",
|
|
19439
|
+
"description": "`dots`",
|
|
19440
|
+
"type": "`boolean`",
|
|
19441
|
+
"default": "`false`"
|
|
19442
|
+
},
|
|
19443
|
+
{
|
|
19444
|
+
"name": "`label`",
|
|
19445
|
+
"description": "`label`",
|
|
19446
|
+
"type": "`string`",
|
|
19447
|
+
"default": "`dataKey`"
|
|
19448
|
+
}
|
|
19449
|
+
]
|
|
19450
|
+
},
|
|
19451
|
+
{
|
|
19452
|
+
"heading": "Bar (`bar`)",
|
|
19453
|
+
"rows": [
|
|
19454
|
+
{
|
|
19455
|
+
"name": "`color`",
|
|
19456
|
+
"description": "`color`",
|
|
19457
|
+
"type": "`ColorAccessor`",
|
|
19458
|
+
"default": "Auto from theme palette"
|
|
19459
|
+
},
|
|
19460
|
+
{
|
|
19461
|
+
"name": "`opacity`",
|
|
19462
|
+
"description": "`opacity`",
|
|
19463
|
+
"type": "`number`",
|
|
19464
|
+
"default": "`1`"
|
|
19465
|
+
},
|
|
19466
|
+
{
|
|
19467
|
+
"name": "`radius`",
|
|
19468
|
+
"description": "`radius`",
|
|
19469
|
+
"type": "`number`",
|
|
19470
|
+
"default": "`4`"
|
|
19471
|
+
},
|
|
19472
|
+
{
|
|
19473
|
+
"name": "`stack`",
|
|
19474
|
+
"description": "`stack`",
|
|
19475
|
+
"type": "`string`",
|
|
19476
|
+
"default": "`undefined`"
|
|
19477
|
+
},
|
|
19478
|
+
{
|
|
19479
|
+
"name": "`group`",
|
|
19480
|
+
"description": "`group`",
|
|
19481
|
+
"type": "`string`",
|
|
19482
|
+
"default": "`undefined`"
|
|
19483
|
+
},
|
|
19484
|
+
{
|
|
19485
|
+
"name": "`label`",
|
|
19486
|
+
"description": "`label`",
|
|
19487
|
+
"type": "`string`",
|
|
19488
|
+
"default": "`dataKey`"
|
|
19489
|
+
}
|
|
19490
|
+
]
|
|
19491
|
+
},
|
|
19492
|
+
{
|
|
19493
|
+
"heading": "Area (`area`)",
|
|
19494
|
+
"rows": [
|
|
19495
|
+
{
|
|
19496
|
+
"name": "`color`",
|
|
19497
|
+
"description": "`color`",
|
|
19498
|
+
"type": "`string`",
|
|
19499
|
+
"default": "Auto from theme palette"
|
|
19500
|
+
},
|
|
19501
|
+
{
|
|
19502
|
+
"name": "`opacity`",
|
|
19503
|
+
"description": "`opacity`",
|
|
19504
|
+
"type": "`number`",
|
|
19505
|
+
"default": "`0.3`"
|
|
19506
|
+
},
|
|
19507
|
+
{
|
|
19508
|
+
"name": "`curve`",
|
|
19509
|
+
"description": "`curve`",
|
|
19510
|
+
"type": "`'linear' \\",
|
|
19511
|
+
"default": "'monotone' \\"
|
|
19512
|
+
},
|
|
19513
|
+
{
|
|
19514
|
+
"name": "`stack`",
|
|
19515
|
+
"description": "`stack`",
|
|
19516
|
+
"type": "`string`",
|
|
19517
|
+
"default": "`undefined`"
|
|
19518
|
+
},
|
|
19519
|
+
{
|
|
19520
|
+
"name": "`gradient`",
|
|
19521
|
+
"description": "`gradient`",
|
|
19522
|
+
"type": "`boolean`",
|
|
19523
|
+
"default": "`false`"
|
|
19524
|
+
},
|
|
19525
|
+
{
|
|
19526
|
+
"name": "`stroke`",
|
|
19527
|
+
"description": "`stroke`",
|
|
19528
|
+
"type": "`boolean`",
|
|
19529
|
+
"default": "`true`"
|
|
19530
|
+
},
|
|
19531
|
+
{
|
|
19532
|
+
"name": "`label`",
|
|
19533
|
+
"description": "`label`",
|
|
19534
|
+
"type": "`string`",
|
|
19535
|
+
"default": "`dataKey`"
|
|
19536
|
+
}
|
|
19537
|
+
]
|
|
19538
|
+
},
|
|
19539
|
+
{
|
|
19540
|
+
"heading": "Dot (`dot`)",
|
|
19541
|
+
"rows": [
|
|
19542
|
+
{
|
|
19543
|
+
"name": "`color`",
|
|
19544
|
+
"description": "`color`",
|
|
19545
|
+
"type": "`ColorAccessor`",
|
|
19546
|
+
"default": "Auto from theme palette"
|
|
19547
|
+
},
|
|
19548
|
+
{
|
|
19549
|
+
"name": "`radius`",
|
|
19550
|
+
"description": "`radius`",
|
|
19551
|
+
"type": "`number`",
|
|
19552
|
+
"default": "`4`"
|
|
19553
|
+
},
|
|
19554
|
+
{
|
|
19555
|
+
"name": "`opacity`",
|
|
19556
|
+
"description": "`opacity`",
|
|
19557
|
+
"type": "`number`",
|
|
19558
|
+
"default": "`0.8`"
|
|
19559
|
+
},
|
|
19560
|
+
{
|
|
19561
|
+
"name": "`dodge`",
|
|
19562
|
+
"description": "`dodge`",
|
|
19563
|
+
"type": "`boolean`",
|
|
19564
|
+
"default": "`false`"
|
|
19565
|
+
},
|
|
19566
|
+
{
|
|
19567
|
+
"name": "`label`",
|
|
19568
|
+
"description": "`label`",
|
|
19569
|
+
"type": "`string`",
|
|
19570
|
+
"default": "`dataKey`"
|
|
19571
|
+
}
|
|
19572
|
+
]
|
|
19573
|
+
},
|
|
19574
|
+
{
|
|
19575
|
+
"heading": "Band (`band`)",
|
|
19576
|
+
"rows": [
|
|
19577
|
+
{
|
|
19578
|
+
"name": "`upper`",
|
|
19579
|
+
"description": "`upper`",
|
|
19580
|
+
"type": "`string`",
|
|
19581
|
+
"default": "**Required**"
|
|
19582
|
+
},
|
|
19583
|
+
{
|
|
19584
|
+
"name": "`lower`",
|
|
19585
|
+
"description": "`lower`",
|
|
19586
|
+
"type": "`string`",
|
|
19587
|
+
"default": "**Required**"
|
|
19588
|
+
},
|
|
19589
|
+
{
|
|
19590
|
+
"name": "`color`",
|
|
19591
|
+
"description": "`color`",
|
|
19592
|
+
"type": "`string`",
|
|
19593
|
+
"default": "`'var(--color-accent)'`"
|
|
19594
|
+
},
|
|
19595
|
+
{
|
|
19596
|
+
"name": "`opacity`",
|
|
19597
|
+
"description": "`opacity`",
|
|
19598
|
+
"type": "`number`",
|
|
19599
|
+
"default": "`0.15`"
|
|
19600
|
+
}
|
|
19601
|
+
]
|
|
19602
|
+
},
|
|
19603
|
+
{
|
|
19604
|
+
"heading": "Reference line (`referenceLine`)",
|
|
19605
|
+
"rows": [
|
|
19606
|
+
{
|
|
19607
|
+
"name": "`y`",
|
|
19608
|
+
"description": "`y`",
|
|
19609
|
+
"type": "`number`",
|
|
19610
|
+
"default": "`undefined`"
|
|
19611
|
+
},
|
|
19612
|
+
{
|
|
19613
|
+
"name": "`y2`",
|
|
19614
|
+
"description": "`y2`",
|
|
19615
|
+
"type": "`number`",
|
|
19616
|
+
"default": "`undefined`"
|
|
19617
|
+
},
|
|
19618
|
+
{
|
|
19619
|
+
"name": "`x`",
|
|
19620
|
+
"description": "`x`",
|
|
19621
|
+
"type": "`number`",
|
|
19622
|
+
"default": "`undefined`"
|
|
19623
|
+
},
|
|
19624
|
+
{
|
|
19625
|
+
"name": "`color`",
|
|
19626
|
+
"description": "`color`",
|
|
19627
|
+
"type": "`string`",
|
|
19628
|
+
"default": "`'var(--astryx-color-border-emphasized)'`"
|
|
19629
|
+
},
|
|
19630
|
+
{
|
|
19631
|
+
"name": "`strokeWidth`",
|
|
19632
|
+
"description": "`strokeWidth`",
|
|
19633
|
+
"type": "`number`",
|
|
19634
|
+
"default": "`1`"
|
|
19635
|
+
},
|
|
19636
|
+
{
|
|
19637
|
+
"name": "`strokeDasharray`",
|
|
19638
|
+
"description": "`strokeDasharray`",
|
|
19639
|
+
"type": "`string`",
|
|
19640
|
+
"default": "`undefined`"
|
|
19641
|
+
},
|
|
19642
|
+
{
|
|
19643
|
+
"name": "`label`",
|
|
19644
|
+
"description": "`label`",
|
|
19645
|
+
"type": "`string`",
|
|
19646
|
+
"default": "`undefined`"
|
|
19647
|
+
},
|
|
19648
|
+
{
|
|
19649
|
+
"name": "`labelPosition`",
|
|
19650
|
+
"description": "`labelPosition`",
|
|
19651
|
+
"type": "`'start' \\",
|
|
19652
|
+
"default": "'end'`"
|
|
19653
|
+
},
|
|
19654
|
+
{
|
|
19655
|
+
"name": "`bandOpacity`",
|
|
19656
|
+
"description": "`bandOpacity`",
|
|
19657
|
+
"type": "`number`",
|
|
19658
|
+
"default": "`0.1`"
|
|
19659
|
+
}
|
|
19660
|
+
]
|
|
19661
|
+
},
|
|
19662
|
+
{
|
|
19663
|
+
"heading": "Error bar (`errorBar`)",
|
|
19664
|
+
"rows": [
|
|
19665
|
+
{
|
|
19666
|
+
"name": "`high`",
|
|
19667
|
+
"description": "`high`",
|
|
19668
|
+
"type": "`string`",
|
|
19669
|
+
"default": "**Required**"
|
|
19670
|
+
},
|
|
19671
|
+
{
|
|
19672
|
+
"name": "`low`",
|
|
19673
|
+
"description": "`low`",
|
|
19674
|
+
"type": "`string`",
|
|
19675
|
+
"default": "**Required**"
|
|
19676
|
+
},
|
|
19677
|
+
{
|
|
19678
|
+
"name": "`color`",
|
|
19679
|
+
"description": "`color`",
|
|
19680
|
+
"type": "`string`",
|
|
19681
|
+
"default": "`'var(--astryx-color-text-primary)'`"
|
|
19682
|
+
},
|
|
19683
|
+
{
|
|
19684
|
+
"name": "`strokeWidth`",
|
|
19685
|
+
"description": "`strokeWidth`",
|
|
19686
|
+
"type": "`number`",
|
|
19687
|
+
"default": "`1.5`"
|
|
19688
|
+
},
|
|
19689
|
+
{
|
|
19690
|
+
"name": "`capWidth`",
|
|
19691
|
+
"description": "`capWidth`",
|
|
19692
|
+
"type": "`number`",
|
|
19693
|
+
"default": "`8`"
|
|
19694
|
+
}
|
|
19695
|
+
]
|
|
19696
|
+
},
|
|
19697
|
+
{
|
|
19698
|
+
"heading": "Candlestick (`candlestick`)",
|
|
19699
|
+
"rows": [
|
|
19700
|
+
{
|
|
19701
|
+
"name": "`open`",
|
|
19702
|
+
"description": "`open`",
|
|
19703
|
+
"type": "`string`",
|
|
19704
|
+
"default": "**Required**"
|
|
19705
|
+
},
|
|
19706
|
+
{
|
|
19707
|
+
"name": "`high`",
|
|
19708
|
+
"description": "`high`",
|
|
19709
|
+
"type": "`string`",
|
|
19710
|
+
"default": "**Required**"
|
|
19711
|
+
},
|
|
19712
|
+
{
|
|
19713
|
+
"name": "`low`",
|
|
19714
|
+
"description": "`low`",
|
|
19715
|
+
"type": "`string`",
|
|
19716
|
+
"default": "**Required**"
|
|
19717
|
+
},
|
|
19718
|
+
{
|
|
19719
|
+
"name": "`close`",
|
|
19720
|
+
"description": "`close`",
|
|
19721
|
+
"type": "`string`",
|
|
19722
|
+
"default": "**Required**"
|
|
19723
|
+
},
|
|
19724
|
+
{
|
|
19725
|
+
"name": "`upColor`",
|
|
19726
|
+
"description": "`upColor`",
|
|
19727
|
+
"type": "`string`",
|
|
19728
|
+
"default": "`'var(--astryx-color-success)'`"
|
|
19729
|
+
},
|
|
19730
|
+
{
|
|
19731
|
+
"name": "`downColor`",
|
|
19732
|
+
"description": "`downColor`",
|
|
19733
|
+
"type": "`string`",
|
|
19734
|
+
"default": "`'var(--astryx-color-error)'`"
|
|
19735
|
+
}
|
|
19736
|
+
]
|
|
19737
|
+
},
|
|
19738
|
+
{
|
|
19739
|
+
"heading": "Axes (`ChartAxis`)",
|
|
19740
|
+
"rows": [
|
|
19741
|
+
{
|
|
19742
|
+
"name": "`position`",
|
|
19743
|
+
"description": "`position`",
|
|
19744
|
+
"type": "`'top' \\",
|
|
19745
|
+
"default": "'right' \\"
|
|
19746
|
+
},
|
|
19747
|
+
{
|
|
19748
|
+
"name": "`tickCount`",
|
|
19749
|
+
"description": "`tickCount`",
|
|
19750
|
+
"type": "`number`",
|
|
19751
|
+
"default": "`5`"
|
|
19752
|
+
},
|
|
19753
|
+
{
|
|
19754
|
+
"name": "`maxTicks`",
|
|
19755
|
+
"description": "`maxTicks`",
|
|
19756
|
+
"type": "`number`",
|
|
19757
|
+
"default": "`undefined`"
|
|
19758
|
+
},
|
|
19759
|
+
{
|
|
19760
|
+
"name": "`tickFormat`",
|
|
19761
|
+
"description": "`tickFormat`",
|
|
19762
|
+
"type": "`(value: unknown) => string`",
|
|
19763
|
+
"default": "`undefined`"
|
|
19764
|
+
},
|
|
19765
|
+
{
|
|
19766
|
+
"name": "`truncate`",
|
|
19767
|
+
"description": "`truncate`",
|
|
19768
|
+
"type": "`number`",
|
|
19769
|
+
"default": "`undefined`"
|
|
19770
|
+
},
|
|
19771
|
+
{
|
|
19772
|
+
"name": "`animated`",
|
|
19773
|
+
"description": "`animated`",
|
|
19774
|
+
"type": "`boolean`",
|
|
19775
|
+
"default": "`true`"
|
|
19776
|
+
},
|
|
19777
|
+
{
|
|
19778
|
+
"name": "`showAxisLine`",
|
|
19779
|
+
"description": "`showAxisLine`",
|
|
19780
|
+
"type": "`boolean`",
|
|
19781
|
+
"default": "`true` on bottom axis only"
|
|
19782
|
+
},
|
|
19783
|
+
{
|
|
19784
|
+
"name": "`showTicks`",
|
|
19785
|
+
"description": "`showTicks`",
|
|
19786
|
+
"type": "`boolean`",
|
|
19787
|
+
"default": "`false`"
|
|
19788
|
+
}
|
|
19789
|
+
]
|
|
19790
|
+
},
|
|
19791
|
+
{
|
|
19792
|
+
"heading": "Grid (`ChartGrid`)",
|
|
19793
|
+
"rows": [
|
|
19794
|
+
{
|
|
19795
|
+
"name": "`horizontal`",
|
|
19796
|
+
"description": "`horizontal`",
|
|
19797
|
+
"type": "`boolean`",
|
|
19798
|
+
"default": "`true`"
|
|
19799
|
+
},
|
|
19800
|
+
{
|
|
19801
|
+
"name": "`vertical`",
|
|
19802
|
+
"description": "`vertical`",
|
|
19803
|
+
"type": "`boolean`",
|
|
19804
|
+
"default": "`false`"
|
|
19805
|
+
},
|
|
19806
|
+
{
|
|
19807
|
+
"name": "`tickCount`",
|
|
19808
|
+
"description": "`tickCount`",
|
|
19809
|
+
"type": "`number`",
|
|
19810
|
+
"default": "`5`"
|
|
19811
|
+
}
|
|
19812
|
+
]
|
|
19813
|
+
},
|
|
19814
|
+
{
|
|
19815
|
+
"heading": "Legend (`ChartLegend`)",
|
|
19816
|
+
"rows": [
|
|
19817
|
+
{
|
|
19818
|
+
"name": "`items`",
|
|
19819
|
+
"description": "`items`",
|
|
19820
|
+
"type": "`LegendItem[]`",
|
|
19821
|
+
"default": "`[]`"
|
|
19822
|
+
},
|
|
19823
|
+
{
|
|
19824
|
+
"name": "`position`",
|
|
19825
|
+
"description": "`position`",
|
|
19826
|
+
"type": "`'top' \\",
|
|
19827
|
+
"default": "'bottom' \\"
|
|
19828
|
+
},
|
|
19829
|
+
{
|
|
19830
|
+
"name": "`alignment`",
|
|
19831
|
+
"description": "`alignment`",
|
|
19832
|
+
"type": "`'start' \\",
|
|
19833
|
+
"default": "'center' \\"
|
|
19834
|
+
}
|
|
19835
|
+
]
|
|
19836
|
+
},
|
|
19837
|
+
{
|
|
19838
|
+
"heading": "Tooltip (`ChartTooltip`)",
|
|
19839
|
+
"rows": [
|
|
19840
|
+
{
|
|
19841
|
+
"name": "`series`",
|
|
19842
|
+
"description": "`series`",
|
|
19843
|
+
"type": "`readonly SeriesDef[]`",
|
|
19844
|
+
"default": "Injected by Chart"
|
|
19845
|
+
},
|
|
19846
|
+
{
|
|
19847
|
+
"name": "`hoverIndicator`",
|
|
19848
|
+
"description": "`hoverIndicator`",
|
|
19849
|
+
"type": "`boolean`",
|
|
19850
|
+
"default": "`true`"
|
|
19851
|
+
},
|
|
19852
|
+
{
|
|
19853
|
+
"name": "`showHoverDots`",
|
|
19854
|
+
"description": "`showHoverDots`",
|
|
19855
|
+
"type": "`boolean`",
|
|
19856
|
+
"default": "`true`"
|
|
19857
|
+
},
|
|
19858
|
+
{
|
|
19859
|
+
"name": "`placement`",
|
|
19860
|
+
"description": "`placement`",
|
|
19861
|
+
"type": "`'auto' \\",
|
|
19862
|
+
"default": "'right' \\"
|
|
19863
|
+
}
|
|
19864
|
+
]
|
|
19865
|
+
},
|
|
19866
|
+
{
|
|
19867
|
+
"heading": "Swatch (`ChartSwatch`)",
|
|
19868
|
+
"rows": [{
|
|
19869
|
+
"name": "`color`",
|
|
19870
|
+
"description": "`color`",
|
|
19871
|
+
"type": "`string`",
|
|
19872
|
+
"default": "**Required**"
|
|
19873
|
+
}, {
|
|
19874
|
+
"name": "`variant`",
|
|
19875
|
+
"description": "`variant`",
|
|
19876
|
+
"type": "`'square' \\",
|
|
19877
|
+
"default": "'line'`"
|
|
19878
|
+
}]
|
|
19879
|
+
},
|
|
19880
|
+
{
|
|
19881
|
+
"heading": "Props",
|
|
19882
|
+
"rows": [
|
|
19883
|
+
{
|
|
19884
|
+
"name": "`data`",
|
|
19885
|
+
"type": "`Record[]`",
|
|
19886
|
+
"default": "**Required**",
|
|
19887
|
+
"description": "Rows of data to render. Each row contains an x-axis field and one or more series fields."
|
|
19888
|
+
},
|
|
19889
|
+
{
|
|
19890
|
+
"name": "`xKey`",
|
|
19891
|
+
"type": "`string`",
|
|
19892
|
+
"default": "**Required**",
|
|
19893
|
+
"description": "Field name used as the x value across all series."
|
|
19894
|
+
},
|
|
19895
|
+
{
|
|
19896
|
+
"name": "`series`",
|
|
19897
|
+
"type": "`SeriesDef[]`",
|
|
19898
|
+
"default": "**Required**",
|
|
19899
|
+
"description": "Array of series definitions, rendered in order on a shared coordinate system."
|
|
19900
|
+
},
|
|
19901
|
+
{
|
|
19902
|
+
"name": "`height`",
|
|
19903
|
+
"type": "`number`",
|
|
19904
|
+
"default": "`300`",
|
|
19905
|
+
"description": "Chart height in CSS pixels. Non-finite values and negative numbers are treated as 0."
|
|
19906
|
+
},
|
|
19907
|
+
{
|
|
19908
|
+
"name": "`width`",
|
|
19909
|
+
"type": "`number`",
|
|
19910
|
+
"default": "`0`",
|
|
19911
|
+
"description": "Chart width in CSS pixels. Ignored after the browser first measures the container. Used during server-side rendering when no DOM container exists."
|
|
19912
|
+
},
|
|
19913
|
+
{
|
|
19914
|
+
"name": "`margin`",
|
|
19915
|
+
"type": "`Partial`",
|
|
19916
|
+
"default": "`{ top: 24, right: 24, bottom: 32, left: 48 }`",
|
|
19917
|
+
"description": "Partial margin overrides for the plotting area. Omitted sides use defaults."
|
|
19918
|
+
},
|
|
19919
|
+
{
|
|
19920
|
+
"name": "`yBaseline`",
|
|
19921
|
+
"type": "`'auto' \\",
|
|
19922
|
+
"default": "'zero' \\",
|
|
19923
|
+
"description": "'data'`"
|
|
19924
|
+
},
|
|
19925
|
+
{
|
|
19926
|
+
"name": "`yDomain`",
|
|
19927
|
+
"type": "`[number, number]`",
|
|
19928
|
+
"default": "`undefined`",
|
|
19929
|
+
"description": "Explicit y-axis range `[min, max]`. When set, this range is used directly without baseline adjustments or edge padding."
|
|
19930
|
+
},
|
|
19931
|
+
{
|
|
19932
|
+
"name": "`xDomain`",
|
|
19933
|
+
"type": "`[number, number]`",
|
|
19934
|
+
"default": "`undefined`",
|
|
19935
|
+
"description": "Explicit range `[min, max]` for numeric x-axes. Stays active even when `data` is empty, keeping streaming windows stable. Ignored on categorical band axes."
|
|
19936
|
+
},
|
|
19937
|
+
{
|
|
19938
|
+
"name": "`legend`",
|
|
19939
|
+
"type": "`boolean \\",
|
|
19940
|
+
"default": "ChartLegendConfig`",
|
|
19941
|
+
"description": "`false`"
|
|
19942
|
+
},
|
|
19943
|
+
{
|
|
19944
|
+
"name": "`tooltip`",
|
|
19945
|
+
"type": "`boolean \\",
|
|
19946
|
+
"default": "ChartTooltipConfig`",
|
|
19947
|
+
"description": "`false`"
|
|
19948
|
+
},
|
|
19949
|
+
{
|
|
19950
|
+
"name": "`title`",
|
|
19951
|
+
"type": "`string`",
|
|
19952
|
+
"default": "`undefined`",
|
|
19953
|
+
"description": "Chart title, used as the SVG accessible name."
|
|
19954
|
+
},
|
|
19955
|
+
{
|
|
19956
|
+
"name": "`subtitle`",
|
|
19957
|
+
"type": "`string`",
|
|
19958
|
+
"default": "`undefined`",
|
|
19959
|
+
"description": "Supporting text below the title, used as the SVG accessible description."
|
|
19960
|
+
}
|
|
19961
|
+
]
|
|
19962
|
+
}
|
|
19963
|
+
],
|
|
19964
|
+
"zh": [
|
|
19965
|
+
{
|
|
19966
|
+
"heading": "折线(line)",
|
|
19967
|
+
"rows": [
|
|
19968
|
+
{
|
|
19969
|
+
"name": "`color`",
|
|
19970
|
+
"description": "`color`",
|
|
19971
|
+
"type": "`string`",
|
|
19972
|
+
"default": "自动取色"
|
|
19973
|
+
},
|
|
19974
|
+
{
|
|
19975
|
+
"name": "`curve`",
|
|
19976
|
+
"description": "`curve`",
|
|
19977
|
+
"type": "`'linear' \\",
|
|
19978
|
+
"default": "'monotone' \\"
|
|
19979
|
+
},
|
|
19980
|
+
{
|
|
19981
|
+
"name": "`strokeWidth`",
|
|
19982
|
+
"description": "`strokeWidth`",
|
|
19983
|
+
"type": "`number`",
|
|
19984
|
+
"default": "`2`"
|
|
19985
|
+
},
|
|
19986
|
+
{
|
|
19987
|
+
"name": "`dots`",
|
|
19988
|
+
"description": "`dots`",
|
|
19989
|
+
"type": "`boolean`",
|
|
19990
|
+
"default": "`false`"
|
|
19991
|
+
},
|
|
19992
|
+
{
|
|
19993
|
+
"name": "`label`",
|
|
19994
|
+
"description": "`label`",
|
|
19995
|
+
"type": "`string`",
|
|
19996
|
+
"default": "`dataKey`"
|
|
19997
|
+
}
|
|
19998
|
+
]
|
|
19999
|
+
},
|
|
20000
|
+
{
|
|
20001
|
+
"heading": "柱状(bar)",
|
|
20002
|
+
"rows": [
|
|
20003
|
+
{
|
|
20004
|
+
"name": "`color`",
|
|
20005
|
+
"description": "`color`",
|
|
20006
|
+
"type": "`ColorAccessor`",
|
|
20007
|
+
"default": "自动取色"
|
|
20008
|
+
},
|
|
20009
|
+
{
|
|
20010
|
+
"name": "`opacity`",
|
|
20011
|
+
"description": "`opacity`",
|
|
20012
|
+
"type": "`number`",
|
|
20013
|
+
"default": "`1`"
|
|
20014
|
+
},
|
|
20015
|
+
{
|
|
20016
|
+
"name": "`radius`",
|
|
20017
|
+
"description": "`radius`",
|
|
20018
|
+
"type": "`number`",
|
|
20019
|
+
"default": "`4`"
|
|
20020
|
+
},
|
|
20021
|
+
{
|
|
20022
|
+
"name": "`stack`",
|
|
20023
|
+
"description": "`stack`",
|
|
20024
|
+
"type": "`string`",
|
|
20025
|
+
"default": "`undefined`"
|
|
20026
|
+
},
|
|
20027
|
+
{
|
|
20028
|
+
"name": "`group`",
|
|
20029
|
+
"description": "`group`",
|
|
20030
|
+
"type": "`string`",
|
|
20031
|
+
"default": "`undefined`"
|
|
20032
|
+
},
|
|
20033
|
+
{
|
|
20034
|
+
"name": "`label`",
|
|
20035
|
+
"description": "`label`",
|
|
20036
|
+
"type": "`string`",
|
|
20037
|
+
"default": "`dataKey`"
|
|
20038
|
+
}
|
|
20039
|
+
]
|
|
20040
|
+
},
|
|
20041
|
+
{
|
|
20042
|
+
"heading": "面积(area)",
|
|
20043
|
+
"rows": [
|
|
20044
|
+
{
|
|
20045
|
+
"name": "`color`",
|
|
20046
|
+
"description": "`color`",
|
|
20047
|
+
"type": "`string`",
|
|
20048
|
+
"default": "自动取色"
|
|
20049
|
+
},
|
|
20050
|
+
{
|
|
20051
|
+
"name": "`opacity`",
|
|
20052
|
+
"description": "`opacity`",
|
|
20053
|
+
"type": "`number`",
|
|
20054
|
+
"default": "`0.3`"
|
|
20055
|
+
},
|
|
20056
|
+
{
|
|
20057
|
+
"name": "`curve`",
|
|
20058
|
+
"description": "`curve`",
|
|
20059
|
+
"type": "`'linear' \\",
|
|
20060
|
+
"default": "'monotone' \\"
|
|
20061
|
+
},
|
|
20062
|
+
{
|
|
20063
|
+
"name": "`stack`",
|
|
20064
|
+
"description": "`stack`",
|
|
20065
|
+
"type": "`string`",
|
|
20066
|
+
"default": "`undefined`"
|
|
20067
|
+
},
|
|
20068
|
+
{
|
|
20069
|
+
"name": "`gradient`",
|
|
20070
|
+
"description": "`gradient`",
|
|
20071
|
+
"type": "`boolean`",
|
|
20072
|
+
"default": "`false`"
|
|
20073
|
+
},
|
|
20074
|
+
{
|
|
20075
|
+
"name": "`stroke`",
|
|
20076
|
+
"description": "`stroke`",
|
|
20077
|
+
"type": "`boolean`",
|
|
20078
|
+
"default": "`true`"
|
|
20079
|
+
},
|
|
20080
|
+
{
|
|
20081
|
+
"name": "`label`",
|
|
20082
|
+
"description": "`label`",
|
|
20083
|
+
"type": "`string`",
|
|
20084
|
+
"default": "`dataKey`"
|
|
20085
|
+
}
|
|
20086
|
+
]
|
|
20087
|
+
},
|
|
20088
|
+
{
|
|
20089
|
+
"heading": "散点(dot)",
|
|
20090
|
+
"rows": [
|
|
20091
|
+
{
|
|
20092
|
+
"name": "`color`",
|
|
20093
|
+
"description": "`color`",
|
|
20094
|
+
"type": "`ColorAccessor`",
|
|
20095
|
+
"default": "自动取色"
|
|
20096
|
+
},
|
|
20097
|
+
{
|
|
20098
|
+
"name": "`radius`",
|
|
20099
|
+
"description": "`radius`",
|
|
20100
|
+
"type": "`number`",
|
|
20101
|
+
"default": "`4`"
|
|
20102
|
+
},
|
|
20103
|
+
{
|
|
20104
|
+
"name": "`opacity`",
|
|
20105
|
+
"description": "`opacity`",
|
|
20106
|
+
"type": "`number`",
|
|
20107
|
+
"default": "`0.8`"
|
|
20108
|
+
},
|
|
20109
|
+
{
|
|
20110
|
+
"name": "`dodge`",
|
|
20111
|
+
"description": "`dodge`",
|
|
20112
|
+
"type": "`boolean`",
|
|
20113
|
+
"default": "`false`"
|
|
20114
|
+
},
|
|
20115
|
+
{
|
|
20116
|
+
"name": "`label`",
|
|
20117
|
+
"description": "`label`",
|
|
20118
|
+
"type": "`string`",
|
|
20119
|
+
"default": "`dataKey`"
|
|
20120
|
+
}
|
|
20121
|
+
]
|
|
20122
|
+
},
|
|
20123
|
+
{
|
|
20124
|
+
"heading": "区间带(band)",
|
|
20125
|
+
"rows": [
|
|
20126
|
+
{
|
|
20127
|
+
"name": "`upper`",
|
|
20128
|
+
"description": "`upper`",
|
|
20129
|
+
"type": "`string`",
|
|
20130
|
+
"default": "**必填**"
|
|
20131
|
+
},
|
|
20132
|
+
{
|
|
20133
|
+
"name": "`lower`",
|
|
20134
|
+
"description": "`lower`",
|
|
20135
|
+
"type": "`string`",
|
|
20136
|
+
"default": "**必填**"
|
|
20137
|
+
},
|
|
20138
|
+
{
|
|
20139
|
+
"name": "`color`",
|
|
20140
|
+
"description": "`color`",
|
|
20141
|
+
"type": "`string`",
|
|
20142
|
+
"default": "`'var(--color-accent)'`"
|
|
20143
|
+
},
|
|
20144
|
+
{
|
|
20145
|
+
"name": "`opacity`",
|
|
20146
|
+
"description": "`opacity`",
|
|
20147
|
+
"type": "`number`",
|
|
20148
|
+
"default": "`0.15`"
|
|
20149
|
+
}
|
|
20150
|
+
]
|
|
20151
|
+
},
|
|
20152
|
+
{
|
|
20153
|
+
"heading": "参考线(referenceLine)",
|
|
20154
|
+
"rows": [
|
|
20155
|
+
{
|
|
20156
|
+
"name": "`y`",
|
|
20157
|
+
"description": "`y`",
|
|
20158
|
+
"type": "`number`",
|
|
20159
|
+
"default": "`undefined`"
|
|
20160
|
+
},
|
|
20161
|
+
{
|
|
20162
|
+
"name": "`y2`",
|
|
20163
|
+
"description": "`y2`",
|
|
20164
|
+
"type": "`number`",
|
|
20165
|
+
"default": "`undefined`"
|
|
20166
|
+
},
|
|
20167
|
+
{
|
|
20168
|
+
"name": "`x`",
|
|
20169
|
+
"description": "`x`",
|
|
20170
|
+
"type": "`number`",
|
|
20171
|
+
"default": "`undefined`"
|
|
20172
|
+
},
|
|
20173
|
+
{
|
|
20174
|
+
"name": "`color`",
|
|
20175
|
+
"description": "`color`",
|
|
20176
|
+
"type": "`string`",
|
|
20177
|
+
"default": "`'var(--astryx-color-border-emphasized)'`"
|
|
20178
|
+
},
|
|
20179
|
+
{
|
|
20180
|
+
"name": "`strokeWidth`",
|
|
20181
|
+
"description": "`strokeWidth`",
|
|
20182
|
+
"type": "`number`",
|
|
20183
|
+
"default": "`1`"
|
|
20184
|
+
},
|
|
20185
|
+
{
|
|
20186
|
+
"name": "`strokeDasharray`",
|
|
20187
|
+
"description": "`strokeDasharray`",
|
|
20188
|
+
"type": "`string`",
|
|
20189
|
+
"default": "`undefined`"
|
|
20190
|
+
},
|
|
20191
|
+
{
|
|
20192
|
+
"name": "`label`",
|
|
20193
|
+
"description": "`label`",
|
|
20194
|
+
"type": "`string`",
|
|
20195
|
+
"default": "`undefined`"
|
|
20196
|
+
},
|
|
20197
|
+
{
|
|
20198
|
+
"name": "`labelPosition`",
|
|
20199
|
+
"description": "`labelPosition`",
|
|
20200
|
+
"type": "`'start' \\",
|
|
20201
|
+
"default": "'end'`"
|
|
20202
|
+
},
|
|
20203
|
+
{
|
|
20204
|
+
"name": "`bandOpacity`",
|
|
20205
|
+
"description": "`bandOpacity`",
|
|
20206
|
+
"type": "`number`",
|
|
20207
|
+
"default": "`0.1`"
|
|
20208
|
+
}
|
|
20209
|
+
]
|
|
20210
|
+
},
|
|
20211
|
+
{
|
|
20212
|
+
"heading": "误差条(errorBar)",
|
|
20213
|
+
"rows": [
|
|
20214
|
+
{
|
|
20215
|
+
"name": "`high`",
|
|
20216
|
+
"description": "`high`",
|
|
20217
|
+
"type": "`string`",
|
|
20218
|
+
"default": "**必填**"
|
|
20219
|
+
},
|
|
20220
|
+
{
|
|
20221
|
+
"name": "`low`",
|
|
20222
|
+
"description": "`low`",
|
|
20223
|
+
"type": "`string`",
|
|
20224
|
+
"default": "**必填**"
|
|
20225
|
+
},
|
|
20226
|
+
{
|
|
20227
|
+
"name": "`color`",
|
|
20228
|
+
"description": "`color`",
|
|
20229
|
+
"type": "`string`",
|
|
20230
|
+
"default": "`'var(--astryx-color-text-primary)'`"
|
|
20231
|
+
},
|
|
20232
|
+
{
|
|
20233
|
+
"name": "`strokeWidth`",
|
|
20234
|
+
"description": "`strokeWidth`",
|
|
20235
|
+
"type": "`number`",
|
|
20236
|
+
"default": "`1.5`"
|
|
20237
|
+
},
|
|
20238
|
+
{
|
|
20239
|
+
"name": "`capWidth`",
|
|
20240
|
+
"description": "`capWidth`",
|
|
20241
|
+
"type": "`number`",
|
|
20242
|
+
"default": "`8`"
|
|
20243
|
+
}
|
|
20244
|
+
]
|
|
20245
|
+
},
|
|
20246
|
+
{
|
|
20247
|
+
"heading": "蜡烛图(candlestick)",
|
|
20248
|
+
"rows": [
|
|
20249
|
+
{
|
|
20250
|
+
"name": "`open`",
|
|
20251
|
+
"description": "`open`",
|
|
20252
|
+
"type": "`string`",
|
|
20253
|
+
"default": "**必填**"
|
|
20254
|
+
},
|
|
20255
|
+
{
|
|
20256
|
+
"name": "`high`",
|
|
20257
|
+
"description": "`high`",
|
|
20258
|
+
"type": "`string`",
|
|
20259
|
+
"default": "**必填**"
|
|
20260
|
+
},
|
|
20261
|
+
{
|
|
20262
|
+
"name": "`low`",
|
|
20263
|
+
"description": "`low`",
|
|
20264
|
+
"type": "`string`",
|
|
20265
|
+
"default": "**必填**"
|
|
20266
|
+
},
|
|
20267
|
+
{
|
|
20268
|
+
"name": "`close`",
|
|
20269
|
+
"description": "`close`",
|
|
20270
|
+
"type": "`string`",
|
|
20271
|
+
"default": "**必填**"
|
|
20272
|
+
},
|
|
20273
|
+
{
|
|
20274
|
+
"name": "`upColor`",
|
|
20275
|
+
"description": "`upColor`",
|
|
20276
|
+
"type": "`string`",
|
|
20277
|
+
"default": "`'var(--astryx-color-success)'`"
|
|
20278
|
+
},
|
|
20279
|
+
{
|
|
20280
|
+
"name": "`downColor`",
|
|
20281
|
+
"description": "`downColor`",
|
|
20282
|
+
"type": "`string`",
|
|
20283
|
+
"default": "`'var(--astryx-color-error)'`"
|
|
20284
|
+
}
|
|
20285
|
+
]
|
|
20286
|
+
},
|
|
20287
|
+
{
|
|
20288
|
+
"heading": "坐标轴(ChartAxis)",
|
|
20289
|
+
"rows": [
|
|
20290
|
+
{
|
|
20291
|
+
"name": "`position`",
|
|
20292
|
+
"type": "`'top' \\",
|
|
20293
|
+
"default": "'right' \\",
|
|
20294
|
+
"description": "'bottom' \\"
|
|
20295
|
+
},
|
|
20296
|
+
{
|
|
20297
|
+
"name": "`tickCount`",
|
|
20298
|
+
"type": "`number`",
|
|
20299
|
+
"default": "`5`",
|
|
20300
|
+
"description": "期望显示的刻度数量(该值为参考值,d3 会计算出最工整的步长)。"
|
|
20301
|
+
},
|
|
20302
|
+
{
|
|
20303
|
+
"name": "`maxTicks`",
|
|
20304
|
+
"type": "`number`",
|
|
20305
|
+
"default": "`undefined`",
|
|
20306
|
+
"description": "允许显示的最大刻度文字数量,超出时会等间隔跳过。"
|
|
20307
|
+
},
|
|
20308
|
+
{
|
|
20309
|
+
"name": "`tickFormat`",
|
|
20310
|
+
"type": "`(value: unknown) => string`",
|
|
20311
|
+
"default": "`undefined`",
|
|
20312
|
+
"description": "自定义刻度文字的格式化函数。"
|
|
20313
|
+
},
|
|
20314
|
+
{
|
|
20315
|
+
"name": "`truncate`",
|
|
20316
|
+
"type": "`number`",
|
|
20317
|
+
"default": "`undefined`",
|
|
20318
|
+
"description": "刻度文字最大字符数,超出截断并追加省略号。"
|
|
20319
|
+
},
|
|
20320
|
+
{
|
|
20321
|
+
"name": "`animated`",
|
|
20322
|
+
"type": "`boolean`",
|
|
20323
|
+
"default": "`true`",
|
|
20324
|
+
"description": "刻度位置发生变化时是否开启平滑过渡动画。"
|
|
20325
|
+
},
|
|
20326
|
+
{
|
|
20327
|
+
"name": "`showAxisLine`",
|
|
20328
|
+
"type": "`boolean`",
|
|
20329
|
+
"default": "仅底轴为 `true`",
|
|
20330
|
+
"description": "是否绘制轴线。如果设置了 `showTicks`,无论哪条轴都会强制画出轴线。"
|
|
20331
|
+
},
|
|
20332
|
+
{
|
|
20333
|
+
"name": "`showTicks`",
|
|
20334
|
+
"type": "`boolean`",
|
|
20335
|
+
"default": "`false`",
|
|
20336
|
+
"description": "是否在每个刻度位置画一小段垂直的短刻度须线。"
|
|
20337
|
+
}
|
|
20338
|
+
]
|
|
20339
|
+
},
|
|
20340
|
+
{
|
|
20341
|
+
"heading": "网格(ChartGrid)",
|
|
20342
|
+
"rows": [
|
|
20343
|
+
{
|
|
20344
|
+
"name": "`horizontal`",
|
|
20345
|
+
"type": "`boolean`",
|
|
20346
|
+
"default": "`true`",
|
|
20347
|
+
"description": "是否绘制水平网格线。"
|
|
20348
|
+
},
|
|
20349
|
+
{
|
|
20350
|
+
"name": "`vertical`",
|
|
20351
|
+
"type": "`boolean`",
|
|
20352
|
+
"default": "`false`",
|
|
20353
|
+
"description": "是否绘制垂直网格线。"
|
|
20354
|
+
},
|
|
20355
|
+
{
|
|
20356
|
+
"name": "`tickCount`",
|
|
20357
|
+
"type": "`number`",
|
|
20358
|
+
"default": "`5`",
|
|
20359
|
+
"description": "大约绘制几条网格线。与 `` 的 `tickCount` 保持一致可使网格与刻度完全对齐。"
|
|
20360
|
+
}
|
|
20361
|
+
]
|
|
20362
|
+
},
|
|
20363
|
+
{
|
|
20364
|
+
"heading": "图例(ChartLegend)",
|
|
20365
|
+
"rows": [
|
|
20366
|
+
{
|
|
20367
|
+
"name": "`items`",
|
|
20368
|
+
"type": "`LegendItem[]`",
|
|
20369
|
+
"default": "`[]`",
|
|
20370
|
+
"description": "显式指定的图例项数组。不传则自动从系列推导。"
|
|
20371
|
+
},
|
|
20372
|
+
{
|
|
20373
|
+
"name": "`position`",
|
|
20374
|
+
"type": "`'top' \\",
|
|
20375
|
+
"default": "'bottom' \\",
|
|
20376
|
+
"description": "'start' \\"
|
|
20377
|
+
},
|
|
20378
|
+
{
|
|
20379
|
+
"name": "`alignment`",
|
|
20380
|
+
"type": "`'start' \\",
|
|
20381
|
+
"default": "'center' \\",
|
|
20382
|
+
"description": "'end'`"
|
|
20383
|
+
}
|
|
20384
|
+
]
|
|
20385
|
+
},
|
|
20386
|
+
{
|
|
20387
|
+
"heading": "提示框(ChartTooltip)",
|
|
20388
|
+
"rows": [
|
|
20389
|
+
{
|
|
20390
|
+
"name": "`series`",
|
|
20391
|
+
"type": "`readonly SeriesDef[]`",
|
|
20392
|
+
"default": "由 Chart 注入",
|
|
20393
|
+
"description": "系列定义。独立使用该组件时需要传入同一份 `series` 数组。"
|
|
20394
|
+
},
|
|
20395
|
+
{
|
|
20396
|
+
"name": "`hoverIndicator`",
|
|
20397
|
+
"type": "`boolean`",
|
|
20398
|
+
"default": "`true`",
|
|
20399
|
+
"description": "悬停时是否绘制辅助指示。柱状图高亮整列,折线、面积和散点绘制垂直指示线。"
|
|
20400
|
+
},
|
|
20401
|
+
{
|
|
20402
|
+
"name": "`showHoverDots`",
|
|
20403
|
+
"type": "`boolean`",
|
|
20404
|
+
"default": "`true`",
|
|
20405
|
+
"description": "是否在悬停的数据点上显示高亮小圆点(柱状图自动跳过)。"
|
|
20406
|
+
},
|
|
20407
|
+
{
|
|
20408
|
+
"name": "`placement`",
|
|
20409
|
+
"type": "`'auto' \\",
|
|
20410
|
+
"default": "'right' \\",
|
|
20411
|
+
"description": "'left' \\"
|
|
20412
|
+
}
|
|
20413
|
+
]
|
|
20414
|
+
},
|
|
20415
|
+
{
|
|
20416
|
+
"heading": "颜色色块(ChartSwatch)",
|
|
20417
|
+
"rows": [{
|
|
20418
|
+
"name": "`color`",
|
|
20419
|
+
"type": "`string`",
|
|
20420
|
+
"default": "**必填**",
|
|
20421
|
+
"description": "填充颜色值。"
|
|
20422
|
+
}, {
|
|
20423
|
+
"name": "`variant`",
|
|
20424
|
+
"type": "`'square' \\",
|
|
20425
|
+
"default": "'line'`",
|
|
20426
|
+
"description": "`'square'`"
|
|
20427
|
+
}]
|
|
20428
|
+
},
|
|
20429
|
+
{
|
|
20430
|
+
"heading": "属性",
|
|
20431
|
+
"rows": [
|
|
20432
|
+
{
|
|
20433
|
+
"name": "`data`",
|
|
20434
|
+
"type": "`Record[]`",
|
|
20435
|
+
"default": "**必填**",
|
|
20436
|
+
"description": "要画的数据行。每行既有 x 对应的列,也有一到多个系列的数据列。"
|
|
20437
|
+
},
|
|
20438
|
+
{
|
|
20439
|
+
"name": "`xKey`",
|
|
20440
|
+
"type": "`string`",
|
|
20441
|
+
"default": "**必填**",
|
|
20442
|
+
"description": "每行里当 x 用的字段名,所有系列共用。"
|
|
20443
|
+
},
|
|
20444
|
+
{
|
|
20445
|
+
"name": "`series`",
|
|
20446
|
+
"type": "`SeriesDef[]`",
|
|
20447
|
+
"default": "**必填**",
|
|
20448
|
+
"description": "标记定义数组,按顺序画在同一套坐标系上。"
|
|
20449
|
+
},
|
|
20450
|
+
{
|
|
20451
|
+
"name": "`height`",
|
|
20452
|
+
"type": "`number`",
|
|
20453
|
+
"default": "`300`",
|
|
20454
|
+
"description": "图表高度(单位是 CSS 像素)。非有限值和负数都会当成 0 处理。"
|
|
20455
|
+
},
|
|
20456
|
+
{
|
|
20457
|
+
"name": "`width`",
|
|
20458
|
+
"type": "`number`",
|
|
20459
|
+
"default": "`0`",
|
|
20460
|
+
"description": "图表宽度(单位是 CSS 像素)。浏览器里第一次量到容器宽度后就不再使用;服务端渲染时没有容器可量,全靠它出图。"
|
|
20461
|
+
},
|
|
20462
|
+
{
|
|
20463
|
+
"name": "`margin`",
|
|
20464
|
+
"type": "`Partial`",
|
|
20465
|
+
"default": "`{ top: 24, right: 24, bottom: 32, left: 48 }`",
|
|
20466
|
+
"description": "绘图区内边距的部分覆盖值,没传的边使用默认留白。"
|
|
20467
|
+
},
|
|
20468
|
+
{
|
|
20469
|
+
"name": "`yBaseline`",
|
|
20470
|
+
"type": "`'auto' \\",
|
|
20471
|
+
"default": "'zero' \\",
|
|
20472
|
+
"description": "'data'`"
|
|
20473
|
+
},
|
|
20474
|
+
{
|
|
20475
|
+
"name": "`yDomain`",
|
|
20476
|
+
"type": "`[number, number]`",
|
|
20477
|
+
"default": "`undefined`",
|
|
20478
|
+
"description": "显式指定的 y 轴范围 `[min, max]`。一旦指定就以此为准,不额外加基线、也不留上下余量。"
|
|
20479
|
+
},
|
|
20480
|
+
{
|
|
20481
|
+
"name": "`xDomain`",
|
|
20482
|
+
"type": "`[number, number]`",
|
|
20483
|
+
"default": "`undefined`",
|
|
20484
|
+
"description": "数值 x 轴的显式范围 `[min, max]`。即使 `data` 为空也会照常生效(保证流式更新窗口稳定)。在离散类目的 band 轴上会被忽略。"
|
|
20485
|
+
},
|
|
20486
|
+
{
|
|
20487
|
+
"name": "`legend`",
|
|
20488
|
+
"type": "`boolean \\",
|
|
20489
|
+
"default": "ChartLegendConfig`",
|
|
20490
|
+
"description": "`false`"
|
|
20491
|
+
},
|
|
20492
|
+
{
|
|
20493
|
+
"name": "`tooltip`",
|
|
20494
|
+
"type": "`boolean \\",
|
|
20495
|
+
"default": "ChartTooltipConfig`",
|
|
20496
|
+
"description": "`false`"
|
|
20497
|
+
},
|
|
20498
|
+
{
|
|
20499
|
+
"name": "`title`",
|
|
20500
|
+
"type": "`string`",
|
|
20501
|
+
"default": "`undefined`",
|
|
20502
|
+
"description": "图表标题,同时作为 SVG 的无障碍名称(Accessible Name)。"
|
|
20503
|
+
},
|
|
20504
|
+
{
|
|
20505
|
+
"name": "`subtitle`",
|
|
20506
|
+
"type": "`string`",
|
|
20507
|
+
"default": "`undefined`",
|
|
20508
|
+
"description": "标题下方的补充说明文字,同时作为 SVG 的无障碍描述。"
|
|
20509
|
+
}
|
|
20510
|
+
]
|
|
20511
|
+
}
|
|
20512
|
+
]
|
|
20513
|
+
},
|
|
20514
|
+
"slots": {
|
|
20515
|
+
"en": [{
|
|
20516
|
+
"heading": "Slots",
|
|
20517
|
+
"rows": [
|
|
20518
|
+
{
|
|
20519
|
+
"name": "`grid`",
|
|
20520
|
+
"description": "None"
|
|
20521
|
+
},
|
|
20522
|
+
{
|
|
20523
|
+
"name": "`axes`",
|
|
20524
|
+
"description": "None"
|
|
20525
|
+
},
|
|
20526
|
+
{
|
|
20527
|
+
"name": "`interactions`",
|
|
20528
|
+
"description": "None"
|
|
20529
|
+
},
|
|
20530
|
+
{
|
|
20531
|
+
"name": "`tooltip`",
|
|
20532
|
+
"description": "`{ xValue: unknown, rows: unknown[] }`"
|
|
20533
|
+
},
|
|
20534
|
+
{
|
|
20535
|
+
"name": "`default`",
|
|
20536
|
+
"description": "None"
|
|
20537
|
+
}
|
|
20538
|
+
]
|
|
20539
|
+
}],
|
|
20540
|
+
"zh": []
|
|
20541
|
+
},
|
|
20542
|
+
"events": {
|
|
20543
|
+
"en": [],
|
|
20544
|
+
"zh": [{
|
|
20545
|
+
"heading": "插槽",
|
|
20546
|
+
"rows": [
|
|
20547
|
+
{
|
|
20548
|
+
"name": "`grid`",
|
|
20549
|
+
"payload": "无",
|
|
20550
|
+
"description": "画在系列后方的背景网格,通常放置 ``。"
|
|
20551
|
+
},
|
|
20552
|
+
{
|
|
20553
|
+
"name": "`axes`",
|
|
20554
|
+
"payload": "无",
|
|
20555
|
+
"description": "画在系列后方的坐标轴,通常放置若干个 ``。"
|
|
20556
|
+
},
|
|
20557
|
+
{
|
|
20558
|
+
"name": "`interactions`",
|
|
20559
|
+
"payload": "无",
|
|
20560
|
+
"description": "画在指针捕获层上方的自定义交互图层。"
|
|
20561
|
+
},
|
|
20562
|
+
{
|
|
20563
|
+
"name": "`tooltip`",
|
|
20564
|
+
"payload": "`{ xValue: unknown, rows: unknown[] }`",
|
|
20565
|
+
"description": "提示框的自定义内容插槽。`xValue` 是当前悬停的 x 坐标值,`rows` 是当前行对应各个系列的数据明细。"
|
|
20566
|
+
},
|
|
20567
|
+
{
|
|
20568
|
+
"name": "`default`",
|
|
20569
|
+
"payload": "无",
|
|
20570
|
+
"description": "额外扩展内容,画在内置提示框图层之后。"
|
|
20571
|
+
}
|
|
20572
|
+
]
|
|
20573
|
+
}]
|
|
20574
|
+
},
|
|
20575
|
+
"notes": {
|
|
20576
|
+
"en": [
|
|
20577
|
+
{
|
|
20578
|
+
"heading": "Overview",
|
|
20579
|
+
"text": "`@astryx-vue/charts` uses d3 to calculate scales and geometry, then renders SVG with Vue. Each chart contains a `<Chart>` canvas and one or more series, such as lines, bars, or areas."
|
|
20580
|
+
},
|
|
20581
|
+
{
|
|
20582
|
+
"heading": "Install",
|
|
20583
|
+
"text": "```bash\npnpm add @astryx-vue/charts\n```\n\n`@astryx-vue/charts` depends on `@astryx-vue/core` and `vue`. Make sure both are installed in your project.\n\nThe library includes base styles. Import the full stylesheet in your entry file:\n\n```ts\nimport '@astryx-vue/charts/charts.css'\n```\n\nTo load styles on demand, import individual files from `@astryx-vue/charts/styles/`, such as `chart.css`, `chart-axis.css`, `chart-grid.css`, `chart-legend.css`, `chart-tooltip.css`, or `chart-swatch.css`."
|
|
20584
|
+
},
|
|
20585
|
+
{
|
|
20586
|
+
"heading": "Your first chart",
|
|
20587
|
+
"text": "A minimal chart requires three props on `<Chart>`:\n- `data`: An array of objects where each item represents one row of data.\n- `xKey`: The field name in each row to use for the horizontal x-axis.\n- `series`: An array of series definitions created with functions such as `line()` or `bar()`.\n\nHere is the shortest working line chart:\n\n```vue\n<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, line } from '@astryx-vue/charts'\n\n// 1. Prepare data\nconst data = [\n { month: 'Jan', revenue: 100, cost: 60 },\n { month: 'Feb', revenue: 150, cost: 80 },\n { month: 'Mar', revenue: 120, cost: 70 },\n]\n\n// 2. Define series: two lines reading revenue and cost\nconst series = [\n line('revenue', { label: 'Revenue' }),\n line('cost', { label: 'Cost' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"data\" x-key=\"month\" :series=\"series\" :height=\"260\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>\n```\n\nHere is the live result:"
|
|
20588
|
+
},
|
|
20589
|
+
{
|
|
20590
|
+
"heading": "Series",
|
|
20591
|
+
"text": "A series defines a visual representation of data on the chart. A series object holds internal state during layout and rendering. Because of this, provide each series object to one `<Chart>` only. Create fresh series definitions for each chart instance.\n\nWhen data has missing values (`null`, `undefined`) or non-finite numbers (`NaN`, `Infinity`), line marks break cleanly and leave an empty gap. They never drop to zero or draw false connections across gaps.\n\nWhen `yDomain` is omitted, `yBaseline` controls how the y-axis range is calculated:\n- `'auto'` (default): Marks with a zero baseline (bars, areas) include 0 in the scale. Continuous point marks (lines, dots) keep small padding on both ends so points do not clip at the edges.\n- `'zero'`': Expands the scale symmetrically around 0. Useful for values that swing positive and negative, such as profit and loss.\n- `'data'`: Clamps tightly to the minimum and maximum data values without forcing 0 or adding edge padding."
|
|
20592
|
+
},
|
|
20593
|
+
{
|
|
20594
|
+
"heading": "Line (`line`)",
|
|
20595
|
+
"text": "A line series shows continuous trends over time or across categories.\n\n```ts\ndeclare function line(dataKey: string, options?: LineOptions): SeriesDef\n```"
|
|
20596
|
+
},
|
|
20597
|
+
{
|
|
20598
|
+
"heading": "Bar (`bar`)",
|
|
20599
|
+
"text": "A bar series compares magnitudes across categories or time steps. Bars with the same `group` split the category bandwidth and sit side by side. Bars with the same `stack` stack on top of each other.\n\n```ts\ndeclare function bar(dataKey: string, options?: BarOptions): SeriesDef\n```"
|
|
20600
|
+
},
|
|
20601
|
+
{
|
|
20602
|
+
"heading": "Area (`area`)",
|
|
20603
|
+
"text": "An area series fills the region below a line. It is useful for showing total volume and changing proportions over time. Giving multiple area series the same `stack` name stacks them in order.\n\n```ts\ndeclare function area(dataKey: string, options?: AreaOptions): SeriesDef\n```"
|
|
20604
|
+
},
|
|
20605
|
+
{
|
|
20606
|
+
"heading": "Dot (`dot`)",
|
|
20607
|
+
"text": "A dot series shows the distribution of discrete data points.\n\n```ts\ndeclare function dot(dataKey: string, options?: DotOptions): SeriesDef\n```"
|
|
20608
|
+
},
|
|
20609
|
+
{
|
|
20610
|
+
"heading": "Band (`band`)",
|
|
20611
|
+
"text": "A band series fills a translucent area between an upper and a lower boundary field. It is often used to show forecast confidence intervals. Band marks are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function band(options: BandOptions): SeriesDef\n```"
|
|
20612
|
+
},
|
|
20613
|
+
{
|
|
20614
|
+
"heading": "Reference line (`referenceLine`)",
|
|
20615
|
+
"text": "A reference line marks a specific horizontal or vertical threshold on the chart, or fills a shaded region between two horizontal values. Reference lines are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function referenceLine(options: ReferenceLineOptions): SeriesDef\n```"
|
|
20616
|
+
},
|
|
20617
|
+
{
|
|
20618
|
+
"heading": "Error bar (`errorBar`)",
|
|
20619
|
+
"text": "An error bar (whisker) displays value uncertainty. It is usually paired with `bar()` or `dot()`. Boundary fields use absolute values, allowing asymmetric error intervals. If a row lacks an upper or lower bound, the chart skips that point instead of drawing misleading lines to zero. Error bars are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function errorBar(options: ErrorBarOptions): SeriesDef\n```"
|
|
20620
|
+
},
|
|
20621
|
+
{
|
|
20622
|
+
"heading": "Candlestick (`candlestick`)",
|
|
20623
|
+
"text": "A candlestick series displays open, high, low, and close prices over time, commonly used in financial charts.\n\n```ts\ndeclare function candlestick(options: CandlestickOptions): SeriesDef\n```"
|
|
20624
|
+
},
|
|
20625
|
+
{
|
|
20626
|
+
"heading": "Axes, grid, legend, tooltip",
|
|
20627
|
+
"text": "Supporting chart elements are provided as individual components. You can combine and configure them independently. Setting `legend` or `tooltip` to `true` on `<Chart>` enables their defaults. Pass an object to customize position and layout."
|
|
20628
|
+
},
|
|
20629
|
+
{
|
|
20630
|
+
"heading": "Axes (`ChartAxis`)",
|
|
20631
|
+
"text": "Axes render along chart edges to display ticks and label text."
|
|
20632
|
+
},
|
|
20633
|
+
{
|
|
20634
|
+
"heading": "Grid (`ChartGrid`)",
|
|
20635
|
+
"text": "Grid lines render behind series elements to help align values visually."
|
|
20636
|
+
},
|
|
20637
|
+
{
|
|
20638
|
+
"heading": "Legend (`ChartLegend`)",
|
|
20639
|
+
"text": "A legend lists series names alongside their representative colors. Setting `legend` on `<Chart>` shows the legend (off by default), or use the `<ChartLegend>` component directly."
|
|
20640
|
+
},
|
|
20641
|
+
{
|
|
20642
|
+
"heading": "Tooltip (`ChartTooltip`)",
|
|
20643
|
+
"text": "A tooltip displays details for the data row under the pointer."
|
|
20644
|
+
},
|
|
20645
|
+
{
|
|
20646
|
+
"heading": "Swatch (`ChartSwatch`)",
|
|
20647
|
+
"text": "A swatch renders a small color marker representing a series, useful inside legends or custom tooltips."
|
|
20648
|
+
},
|
|
20649
|
+
{
|
|
20650
|
+
"heading": "Custom tooltip content",
|
|
20651
|
+
"text": "When the default tooltip layout is not enough, customize the card content through the `#tooltip` slot on `<Chart>`. The slot exposes the active `xValue` and row details in `rows`:\n\n```vue\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" tooltip>\n <template #tooltip=\"{ xValue, rows }\">\n <div class=\"my-custom-tooltip\">\n <strong>{{ xValue }}</strong>\n <div v-for=\"row in asRows(rows)\" :key=\"row.key\">\n <ChartSwatch :color=\"row.color\" variant=\"line\" />\n {{ row.label }}: {{ money(row.value) }}\n </div>\n </div>\n </template>\n </Chart>\n</template>\n```"
|
|
20652
|
+
},
|
|
20653
|
+
{
|
|
20654
|
+
"heading": "Colors",
|
|
20655
|
+
"text": "When a series does not set an explicit color, the chart assigns one automatically in order from the active theme palette.\n\nAll chart colors use CSS variables underneath. When switching themes or toggling dark mode, chart colors update automatically without re-rendering the SVG tree.\n\nTo inspect or use the current chart palette programmatically, call `useChartColors()`. It returns a shared object across the package:\n\n- `categorical(n: number)`: Returns an array of `n` categorical colors. If `n` exceeds palette size, colors cycle.\n- `sequential`: Sequential color scale helper supporting nine hues (`blue`, `shamrock`, `orange`, `pink`, `purple`, `red`, `teal`, `yellow`, `gray`). Passing a count `n` returns a gradient color array.\n- `diverging`: Two-pole diverging palette helper. Provides `positiveNegative`, `coldHot`, and `custom` (custom hues on each pole).\n- `semantic`: Semantic status colors, containing `positive`, `negative`, `warning`, and `neutral`.\n- `structural`: Structural colors for chart framing, containing `axis`, `grid`, `tick`, and `label`.\n- `alpha(color: string, opacity: number)`: Generates a color with an alpha channel for a given opacity. CSS variables use `color-mix()` automatically."
|
|
20656
|
+
},
|
|
20657
|
+
{
|
|
20658
|
+
"heading": "Formatting functions",
|
|
20659
|
+
"text": "The library includes formatting helpers for common chart values. Formatting functions follow a safe fallback rule: if an input is `NaN`, `Infinity`, or cannot be parsed, the function returns the original string as-is instead of printing errors or throwing exceptions.\n\n- `compactNumber(value, locale)`: Abbreviates large numbers. For example, `1200` becomes \"1.2K\", `1500000` becomes \"1.5M\", and `1e12` becomes \"1T\".\n- `currency(symbol?, locale?)`: Creates a currency formatter. For example, `currency('$')` can be passed to `ChartAxis`'s `tickFormat`. Values over 1,000 are automatically abbreviated (such as \"$1.5K\").\n- `percent(value, locale)`: Formats a decimal ratio as a percentage. For example, `0.45` becomes \"45%\" and `0.005` becomes \"0.5%\".\n- `shortDate(value, locale)`: Formats dates as a short month and day. For example, \"Jan 5\".\n- `monthYear(value, locale)`: Formats dates as month and year. For example, \"Jan 2024\".\n- `formatList(values, locale)`: Formats a list of words into a natural language string according to locale rules, such as \"A, B, and C\"."
|
|
20660
|
+
},
|
|
20661
|
+
{
|
|
20662
|
+
"heading": "Accessibility",
|
|
20663
|
+
"text": "Charts include built-in accessibility features so screen reader users can understand chart content:\n\n- The root element has a `role=\"img\"` attribute.\n- The chart's accessible name prioritizes the `title` prop. When `title` is omitted, the component builds a semantic description from primary series and the x-axis field (such as \"Monthly Revenue and Cost trend\"). If only annotations are present, it falls back to \"Chart\".\n- The `subtitle` prop maps to the chart's accessible description.\n- When total data points (data rows × data columns) do not exceed 100 (`MAX_TABLE_POINTS`), the chart generates a visually hidden data table using `VisuallyHidden`. Screen readers can read exact values row by row. Above 100 points, the table is omitted to prevent excessive speech noise."
|
|
20664
|
+
},
|
|
20665
|
+
{
|
|
20666
|
+
"heading": "Server-side rendering",
|
|
20667
|
+
"text": "During server-side rendering (SSR), no browser DOM exists, so the component cannot measure parent container width in pixels.\n\nWhen rendering on the server, pass an explicit `width` prop on `<Chart>`. This allows the server to compute scales and render complete SVG paths and rectangles into HTML. After client-side hydration, once the browser measures the real container width, the chart takes over and responds to container resize events."
|
|
20668
|
+
},
|
|
20669
|
+
{
|
|
20670
|
+
"heading": "Not supported yet",
|
|
20671
|
+
"text": "To stay lightweight and focused, the package currently leaves out:\n- Four WebGL-based series marks for very large datasets (such as `dotGL`) are not yet included.\n- The Nuxt module does not yet include auto-import switches for `@astryx-vue/charts`. Import components manually when using Nuxt."
|
|
20672
|
+
},
|
|
20673
|
+
{
|
|
20674
|
+
"heading": "`<Chart>` props and slots",
|
|
20675
|
+
"text": "`<Chart>` is the root container. It measures container dimensions, builds coordinate scales, calculates geometry positions, and renders the SVG tree."
|
|
20676
|
+
}
|
|
20677
|
+
],
|
|
20678
|
+
"zh": [
|
|
20679
|
+
{
|
|
20680
|
+
"heading": "概述",
|
|
20681
|
+
"text": "`@astryx-vue/charts` 使用 d3 计算比例尺和图形路径,再由 Vue 渲染 SVG。每个图表由一个 `<Chart>` 画布和一个或多个系列组成;系列可以是折线、柱状、面积或其他数据标记。"
|
|
20682
|
+
},
|
|
20683
|
+
{
|
|
20684
|
+
"heading": "安装",
|
|
20685
|
+
"text": "```bash\npnpm add @astryx-vue/charts\n```\n\n`@astryx-vue/charts` 依赖同级的 `@astryx-vue/core` 和 `vue`,请确保项目中已经安装了它们。\n\n图表内置了基础样式。你可以在入口文件引入完整样式文件:\n\n```ts\nimport '@astryx-vue/charts/charts.css'\n```\n\n如果你想按需加载样式,也可以只引入 `@astryx-vue/charts/styles/` 目录下的某一个样式文件(例如 `chart.css`、`chart-axis.css`、`chart-grid.css`、`chart-legend.css`、`chart-tooltip.css` 或 `chart-swatch.css`)。"
|
|
20686
|
+
},
|
|
20687
|
+
{
|
|
20688
|
+
"heading": "第一张图",
|
|
20689
|
+
"text": "画一张图最少需要给 `<Chart>` 传三个必填属性:\n- `data`:要展示的数据数组,每一项是一行对象。\n- `xKey`:指定每行对象中哪个字段当作横坐标(x 轴)。\n- `series`:标记系列数组,用 `line()`、`bar()` 等函数生成。\n\n下面是一段最短的可运行折线图代码:\n\n```vue\n<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, line } from '@astryx-vue/charts'\n\n// 1. 准备数据\nconst data = [\n { month: '1月', revenue: 100, cost: 60 },\n { month: '2月', revenue: 150, cost: 80 },\n { month: '3月', revenue: 120, cost: 70 },\n]\n\n// 2. 定义系列:画两条线,一条读 revenue,一条读 cost\nconst series = [\n line('revenue', { label: '收入' }),\n line('cost', { label: '成本' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"data\" x-key=\"month\" :series=\"series\" :height=\"260\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>\n```\n\n实际运行效果如下:"
|
|
20690
|
+
},
|
|
20691
|
+
{
|
|
20692
|
+
"heading": "系列",
|
|
20693
|
+
"text": "系列代表图表上展示的具体数据图形。同一个系列对象在布局和渲染时会被写入内部状态,所以同一个系列对象只能提供给一个 `<Chart>` 使用,每张图都要重新创建一份。\n\n如果数据中存在缺失值(`null`、`undefined`)或非有限数字(`NaN`、`Infinity`),折线会自然断开形成空洞,不会强行掉到 0 基线上画出一条错误虚假的连线。\n\n在不设置显式 `yDomain` 时,y 轴的范围由 `yBaseline` 决定:\n- `'auto'`(默认):带零基线的标记(柱状图、面积图)会把 0 算进坐标范围;只有离散连续点的图表(折线图、散点图等)会在上下两侧保留一点余量,防止数据点贴在画布边缘。\n- `'zero'`:以 0 为中心对称展开坐标轴,适合展示正负波动数据(比如盈利与亏损)。\n- `'data'`:紧贴实际数据的最大值与最小值,不强制包含 0,也不留任何边缘余量。"
|
|
20694
|
+
},
|
|
20695
|
+
{
|
|
20696
|
+
"heading": "折线(line)",
|
|
20697
|
+
"text": "折线用于展示数据随时间或类目的连续变化趋势。\n\n```ts\ndeclare function line(dataKey: string, options?: LineOptions): SeriesDef\n```"
|
|
20698
|
+
},
|
|
20699
|
+
{
|
|
20700
|
+
"heading": "柱状(bar)",
|
|
20701
|
+
"text": "柱状图用于对比不同分类或时间点下的数值大小。相同 `group` 的柱子会把带宽平分并排展示;相同 `stack` 的柱子会一层层堆叠起来展示。\n\n```ts\ndeclare function bar(dataKey: string, options?: BarOptions): SeriesDef\n```"
|
|
20702
|
+
},
|
|
20703
|
+
{
|
|
20704
|
+
"heading": "面积(area)",
|
|
20705
|
+
"text": "面积图在折线下方填充颜色,非常适合表现总量以及各部分占比的变化。如果给多个面积系列指定相同的 `stack` 名字,它们会逐层叠起来。\n\n```ts\ndeclare function area(dataKey: string, options?: AreaOptions): SeriesDef\n```"
|
|
20706
|
+
},
|
|
20707
|
+
{
|
|
20708
|
+
"heading": "散点(dot)",
|
|
20709
|
+
"text": "散点图用于展示离散数据点的分布情况。\n\n```ts\ndeclare function dot(dataKey: string, options?: DotOptions): SeriesDef\n```"
|
|
20710
|
+
},
|
|
20711
|
+
{
|
|
20712
|
+
"heading": "区间带(band)",
|
|
20713
|
+
"text": "区间带用于在上下两个边界字段之间涂满一层半透明的带状区域,通常用来展示预测区间的波动范围。它属于辅助标注标记,不会出现在图例和悬停提示框中。\n\n```ts\ndeclare function band(options: BandOptions): SeriesDef\n```"
|
|
20714
|
+
},
|
|
20715
|
+
{
|
|
20716
|
+
"heading": "参考线(referenceLine)",
|
|
20717
|
+
"text": "参考线可以在图表指定的数值位置画一条横线、竖线,或者在两条横线之间涂上一段浅色参考带,常用于标记阈值或目标值。它属于辅助标注标记,不进入图例和提示框。\n\n```ts\ndeclare function referenceLine(options: ReferenceLineOptions): SeriesDef\n```"
|
|
20718
|
+
},
|
|
20719
|
+
{
|
|
20720
|
+
"heading": "误差条(errorBar)",
|
|
20721
|
+
"text": "误差条(须线)用于展示数值的不确定性范围。它通常配合 `bar()` 或 `dot()` 一起使用。需要注意的是,误差条传入的上下界字段是绝对数值,因此即使上下误差不对称也可以正确绘制。如果某一行缺失了上界或下界,则会直接跳过该点,绝不绘制误导人的零刻度须线。它属于辅助标注标记,不进入图例和提示框。\n\n```ts\ndeclare function errorBar(options: ErrorBarOptions): SeriesDef\n```"
|
|
20722
|
+
},
|
|
20723
|
+
{
|
|
20724
|
+
"heading": "蜡烛图(candlestick)",
|
|
20725
|
+
"text": "蜡烛图(K 线图)用于金融等场景,展示一段时间内的开盘、最高、最低和收盘价。\n\n```ts\ndeclare function candlestick(options: CandlestickOptions): SeriesDef\n```"
|
|
20726
|
+
},
|
|
20727
|
+
{
|
|
20728
|
+
"heading": "坐标轴、网格、图例、提示框",
|
|
20729
|
+
"text": "图表的辅助元素均以独立组件的形式提供,你可以自由组合和调整。在 `<Chart>` 上直接给 `legend` 或 `tooltip` 传 `true` 即可启用默认样式,也可以传配置对象调整位置。"
|
|
20730
|
+
},
|
|
20731
|
+
{
|
|
20732
|
+
"heading": "坐标轴(ChartAxis)",
|
|
20733
|
+
"text": "坐标轴绘制在图表四周,负责标注刻度与文字。"
|
|
20734
|
+
},
|
|
20735
|
+
{
|
|
20736
|
+
"heading": "网格(ChartGrid)",
|
|
20737
|
+
"text": "网格线绘制在数据图形底层,帮助读者对齐数值。"
|
|
20738
|
+
},
|
|
20739
|
+
{
|
|
20740
|
+
"heading": "图例(ChartLegend)",
|
|
20741
|
+
"text": "图例列出各个系列的名称与代表色。在 `<Chart>` 上写 `legend` 就会显示(默认不显示),也可以单独使用 `<ChartLegend>` 组件。"
|
|
20742
|
+
},
|
|
20743
|
+
{
|
|
20744
|
+
"heading": "提示框(ChartTooltip)",
|
|
20745
|
+
"text": "鼠标悬停在图表上时,提示框会展示当前位置对应的数据明细。"
|
|
20746
|
+
},
|
|
20747
|
+
{
|
|
20748
|
+
"heading": "颜色色块(ChartSwatch)",
|
|
20749
|
+
"text": "用于在图例或自定义提示框中绘制一小块系列代表色标。"
|
|
20750
|
+
},
|
|
20751
|
+
{
|
|
20752
|
+
"heading": "自定义提示框内容",
|
|
20753
|
+
"text": "如果内置提示框的样式无法满足要求,可以通过 `<Chart>` 的 `#tooltip` 插槽自定义浮层内容。插槽向外暴露当前悬停的横坐标值 `xValue` 和各系列的数据明细 `rows`:\n\n```vue\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" tooltip>\n <template #tooltip=\"{ xValue, rows }\">\n <div class=\"my-custom-tooltip\">\n <strong>{{ xValue }}</strong>\n <div v-for=\"row in asRows(rows)\" :key=\"row.key\">\n <ChartSwatch :color=\"row.color\" variant=\"line\" />\n {{ row.label }}:{{ money(row.value) }}\n </div>\n </div>\n </template>\n </Chart>\n</template>\n```"
|
|
20754
|
+
},
|
|
20755
|
+
{
|
|
20756
|
+
"heading": "颜色",
|
|
20757
|
+
"text": "如果创建系列时没有显式指定颜色,图表会根据主题里的分类色板,按顺序自动给每个系列分配颜色。\n\n所有的图表颜色底层都是基于 CSS 变量实现的。这意味着当页面切换主题风格,或者从浅色模式切换到深色模式时,图表上的颜色会自动跟随环境刷新,不需要重新渲染整张图表。\n\n如果你需要手动获取当前图表体系的配色方案,可以调用 `useChartColors()` 组合式函数。它返回的对象在整个包中全局共享且保持不变,其包含的成员如下:\n\n- `categorical(n: number)`:获取包含指定数量的分类色数组。数量超出槽位时会自动循环。\n- `sequential`:顺序色阶对象,支持 9 种色相(`blue`、`shamrock`、`orange`、`pink`、`purple`、`red`、`teal`、`yellow`、`gray`)。传入数量 `n` 即可返回渐变过渡色数组。\n- `diverging`:双极发散配色。提供 `positiveNegative`(正负分明)、`coldHot`(冷暖分明)以及 `custom`(自定义两侧色相)方法。\n- `semantic`:语义单色值对象,包含 `positive`、`negative`、`warning` 和 `neutral` 四种标准状态色。\n- `structural`:图表骨架的结构色,包含 `axis`(坐标轴线)、`grid`(网格线)、`tick`(刻度短线)和 `label`(文本标签)四种颜色。\n- `alpha(color: string, opacity: number)`:按指定透明度生成带透明通道的颜色值(若为 CSS 变量则自动采用 `color-mix()` 处理)。"
|
|
20758
|
+
},
|
|
20759
|
+
{
|
|
20760
|
+
"heading": "格式化函数",
|
|
20761
|
+
"text": "图表库内置了一组常用的格式化函数。所有格式化函数都遵循安全原则:如果遇到 `NaN`、`Infinity` 或无法解析的值,会原样返回原字符串,绝对不会在界面上打印出异常信息,也不会报错崩溃。\n\n- `compactNumber(value, locale)`:将大数字缩写。例如 `1200` 转为「1.2K」,`1500000` 转为「1.5M」,`1e12` 转为「1T」。\n- `currency(symbol?, locale?)`:创建货币格式化器。例如 `currency('¥')` 可以直接传给 `ChartAxis` 的 `tickFormat` 属性,数值大于一千时会自动应用缩写(如「¥1.5K」)。\n- `percent(value, locale)`:将小数比例转换为百分比。例如 `0.45` 转换为「45%」,`0.005` 转换为「0.5%」。\n- `shortDate(value, locale)`:将日期格式化为简短的月日。例如「Jan 5」或「3月12日」。\n- `monthYear(value, locale)`:将日期格式化为年月。例如「Jan 2024」或「2025年3月」。\n- `formatList(values, locale)`:按指定语言的语法规则把多个词汇拼接为自然语言列表,例如将数组拼成「甲、乙和丙」。"
|
|
20762
|
+
},
|
|
20763
|
+
{
|
|
20764
|
+
"heading": "无障碍",
|
|
20765
|
+
"text": "图表在可访问性方面做了完善的无障碍设计,让视障人士通过读屏软件也能了解图表内容:\n\n- 整个图表根元素具有 `role=\"img\"` 角色属性。\n- 图表的无障碍名称优先使用传入的 `title` 属性;如果没有提供 `title`,系统会根据包含的主要系列与 x 轴字段自动拼接出清晰的语义名称(如「月度 收入和成本 走势」);如果全是辅助标注线,则默认退化为「图表」。\n- 传入的 `subtitle` 会自动关联为图表的无障碍长描述。\n- 当数据点总数(数据行数 × 数据列数)不超过 100 个(即常量 `MAX_TABLE_POINTS`)时,图表会自动生成一份在视觉上完全隐藏的数据表格(通过 `VisuallyHidden` 隐藏在屏幕外)。读屏软件能够直接逐行朗读表格中的精准数据;如果数据量超过 100 点,则不会生成表格以避免造成大量无效的语音噪音。"
|
|
20766
|
+
},
|
|
20767
|
+
{
|
|
20768
|
+
"heading": "服务端渲染",
|
|
20769
|
+
"text": "在服务端渲染(SSR)环境中运行时,由于不存在真实的浏览器 DOM,组件无法自动测量父级容器的实际像素宽度。\n\n因此,在服务端渲染时,必须在 `<Chart>` 上显式传入 `width` 属性指定宽度像素值,服务端才能正确完成比例尺计算并完整输出包含路径和矩形的 SVG 标签。在浏览器端水合后,图表一旦首次测量到真实的容器宽度,便会自动接管并按真实容器尺寸响应式排版。"
|
|
20770
|
+
},
|
|
20771
|
+
{
|
|
20772
|
+
"heading": "现在不支持的",
|
|
20773
|
+
"text": "为了保持包的轻量和聚焦,目前以下能力暂未包含在当前包中:\n- 4 个基于 WebGL 的超大数据集标记(例如 `dotGL` 等标记)尚未收录于此包中。\n- Nuxt 模块中尚未提供针对 `@astryx-vue/charts` 的自动引入配置开关,在 Nuxt 项目中使用时需手动通过 npm 依赖导入。"
|
|
20774
|
+
},
|
|
20775
|
+
{
|
|
20776
|
+
"heading": "`<Chart>` 的属性与插槽",
|
|
20777
|
+
"text": "`<Chart>` 是整张图表的根容器。它负责测量尺寸、建立坐标系、计算点位并把图形画在 SVG 里。"
|
|
20778
|
+
}
|
|
20779
|
+
]
|
|
20780
|
+
},
|
|
20781
|
+
"examples": [
|
|
20782
|
+
{
|
|
20783
|
+
"title": {
|
|
20784
|
+
"en": "Line chart",
|
|
20785
|
+
"zh": "折线图"
|
|
20786
|
+
},
|
|
20787
|
+
"description": {
|
|
20788
|
+
"en": "Line chart with revenue and cost series, grid lines, axes, a bottom legend, and hover tooltips.",
|
|
20789
|
+
"zh": "包含收入与成本两条折线,带有网格、坐标轴、底部图例以及鼠标悬停提示框。"
|
|
20790
|
+
},
|
|
20791
|
+
"code": "<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, line } from '@astryx-vue/charts'\nimport { monthly } from './data'\n\nconst series = [\n line('revenue', { label: '收入' }),\n line('cost', { label: '成本' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" :height=\"260\" title=\"每月收入与成本\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20792
|
+
},
|
|
20793
|
+
{
|
|
20794
|
+
"title": {
|
|
20795
|
+
"en": "Bar chart",
|
|
20796
|
+
"zh": "柱状图"
|
|
20797
|
+
},
|
|
20798
|
+
"description": {
|
|
20799
|
+
"en": "Bar chart example: revenue and cost share a group to display side by side for each month.",
|
|
20800
|
+
"zh": "柱状图示例:收入和成本共用同一个 group 分组,在每个月份下并排显示。"
|
|
20801
|
+
},
|
|
20802
|
+
"code": "<script setup lang=\"ts\">\nimport { bar, Chart, ChartAxis, ChartGrid } from '@astryx-vue/charts'\nimport { monthly } from './data'\n\n// 没写 group 或 stack 的柱子会各占一列;这里两个系列同属一个 group,并排显示。\nconst series = [\n bar('revenue', { group: 'money', label: '收入', radius: 4 }),\n bar('cost', { group: 'money', label: '成本', radius: 4 }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" :height=\"260\" title=\"每月收入与成本\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20803
|
+
},
|
|
20804
|
+
{
|
|
20805
|
+
"title": {
|
|
20806
|
+
"en": "Stacked area chart",
|
|
20807
|
+
"zh": "堆叠面积图"
|
|
20808
|
+
},
|
|
20809
|
+
"description": {
|
|
20810
|
+
"en": "Stacked area chart: cost and profit share a stack identifier to show overall totals.",
|
|
20811
|
+
"zh": "堆叠面积图:成本与利润共用相同的 stack 标识,堆叠呈现整体总额。"
|
|
20812
|
+
},
|
|
20813
|
+
"code": "<script setup lang=\"ts\">\nimport { area, Chart, ChartAxis, ChartGrid } from '@astryx-vue/charts'\nimport { monthly } from './data'\n\n// stack 名字相同的面积会一层叠一层往上长。\nconst series = [\n area('cost', { stack: 'total', label: '成本' }),\n area('profit', { stack: 'total', label: '利润' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" :height=\"260\" title=\"成本加利润等于收入\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20814
|
+
},
|
|
20815
|
+
{
|
|
20816
|
+
"title": {
|
|
20817
|
+
"en": "Dot chart",
|
|
20818
|
+
"zh": "散点图"
|
|
20819
|
+
},
|
|
20820
|
+
"description": {
|
|
20821
|
+
"en": "Dot chart example: distribution of test scores across sample sizes and control groups.",
|
|
20822
|
+
"zh": "散点图示例:展示不同样本量下的得分与对照组数据点分布。"
|
|
20823
|
+
},
|
|
20824
|
+
"code": "<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, dot } from '@astryx-vue/charts'\nimport { samples } from './data'\n\nconst series = [\n dot('score', { label: '得分', radius: 5 }),\n dot('other', { label: '对照组', radius: 5 }),\n]\n<\/script>\n\n<template>\n <!-- 数值 x 轴默认紧贴数据,最左和最右的点会被切掉一半;用 x-domain 留出边距。 -->\n <Chart :data=\"samples\" x-key=\"size\" :x-domain=\"[0, 70]\" :series=\"series\" :height=\"260\" title=\"样本量与得分\" legend tooltip>\n <template #grid>\n <ChartGrid vertical />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20825
|
+
},
|
|
20826
|
+
{
|
|
20827
|
+
"title": {
|
|
20828
|
+
"en": "Band and reference line",
|
|
20829
|
+
"zh": "区间带与参考线"
|
|
20830
|
+
},
|
|
20831
|
+
"description": {
|
|
20832
|
+
"en": "Band and reference line: shaded forecast interval in the background, actual line in the middle, and a dashed target threshold on top.",
|
|
20833
|
+
"zh": "区间带与参考线:底层绘制预测波动的带状面积,中间绘制实际折线,上方横穿一条虚线目标参考线。"
|
|
20834
|
+
},
|
|
20835
|
+
"code": "<script setup lang=\"ts\">\nimport { band, Chart, ChartAxis, ChartGrid, line, referenceLine } from '@astryx-vue/charts'\nimport { forecast } from './data'\n\n// 区间带画在最前面,折线和参考线叠在它上面。\nconst series = [\n band({ upper: 'high', lower: 'low' }),\n line('actual', { label: '实际值', dots: true }),\n referenceLine({ y: 60, label: '目标 60', strokeDasharray: '4 4' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"forecast\" x-key=\"week\" :series=\"series\" :height=\"260\" title=\"预测区间与目标线\" tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20836
|
+
},
|
|
20837
|
+
{
|
|
20838
|
+
"title": {
|
|
20839
|
+
"en": "Error bars",
|
|
20840
|
+
"zh": "误差条"
|
|
20841
|
+
},
|
|
20842
|
+
"description": {
|
|
20843
|
+
"en": "Error bars: paired with a bar chart to display group averages alongside 95% confidence intervals.",
|
|
20844
|
+
"zh": "误差条:配合柱状图使用,直观展示各实验组均值及对应的 95% 置信区间。"
|
|
20845
|
+
},
|
|
20846
|
+
"code": "<script setup lang=\"ts\">\nimport { bar, Chart, ChartAxis, ChartGrid, errorBar } from '@astryx-vue/charts'\nimport { experiments } from './data'\n\n// errorBar 读的是上下界的绝对值,所以不对称的区间也能画。\nconst series = [\n bar('mean', { label: '均值', radius: 4 }),\n errorBar({ high: 'upper95', low: 'lower95' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"experiments\" x-key=\"group\" :series=\"series\" :height=\"260\" title=\"各组均值与 95% 区间\" tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20847
|
+
},
|
|
20848
|
+
{
|
|
20849
|
+
"title": {
|
|
20850
|
+
"en": "Candlestick",
|
|
20851
|
+
"zh": "蜡烛图"
|
|
20852
|
+
},
|
|
20853
|
+
"description": {
|
|
20854
|
+
"en": "Candlestick chart: displays daily price movements for a stock or commodity over a week.",
|
|
20855
|
+
"zh": "蜡烛图(K 线图):展示一周内股票或商品的价格走势。"
|
|
20856
|
+
},
|
|
20857
|
+
"code": "<script setup lang=\"ts\">\nimport { candlestick, Chart, ChartAxis, ChartGrid } from '@astryx-vue/charts'\nimport { prices } from './data'\n\nconst series = [candlestick({ open: 'open', high: 'high', low: 'low', close: 'close' })]\n<\/script>\n\n<template>\n <Chart :data=\"prices\" x-key=\"day\" :series=\"series\" :height=\"260\" y-baseline=\"data\" title=\"一周价格走势\">\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>"
|
|
20858
|
+
},
|
|
20859
|
+
{
|
|
20860
|
+
"title": {
|
|
20861
|
+
"en": "Custom tooltip",
|
|
20862
|
+
"zh": "自定义提示框"
|
|
20863
|
+
},
|
|
20864
|
+
"description": {
|
|
20865
|
+
"en": "Custom tooltip: uses the #tooltip slot to read active data and render a custom layout.",
|
|
20866
|
+
"zh": "自定义悬停提示框:通过 #tooltip 插槽读取当前数据并按自定义格式排版渲染。"
|
|
20867
|
+
},
|
|
20868
|
+
"code": "<script setup lang=\"ts\">\nimport type { TooltipSeriesValue } from '@astryx-vue/charts'\nimport { Chart, ChartAxis, ChartGrid, ChartSwatch, currency, line } from '@astryx-vue/charts'\nimport { monthly } from './data'\n\nconst money = currency('¥')\n\nconst series = [\n line('revenue', { label: '收入' }),\n line('profit', { label: '利润' }),\n]\n\n// 插槽给出的 rows 是 unknown[],实际每一项就是 TooltipSeriesValue。\nfunction asRows(rows: unknown[]): TooltipSeriesValue[] {\n return rows as TooltipSeriesValue[]\n}\n<\/script>\n\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" :height=\"260\" title=\"收入与利润\" tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" :tick-format=\"money\" />\n </template>\n <template #tooltip=\"{ xValue, rows }\">\n <div style=\"display: grid; gap: 4px; padding: 8px 12px; font-size: 13px\">\n <strong>{{ xValue }}</strong>\n <div v-for=\"row in asRows(rows)\" :key=\"row.key\">\n <ChartSwatch :color=\"row.color\" variant=\"line\" />\n {{ row.label }}:{{ money(row.value) }}\n </div>\n </div>\n </template>\n </Chart>\n</template>"
|
|
20869
|
+
}
|
|
20870
|
+
],
|
|
20871
|
+
"anatomy": {
|
|
20872
|
+
"en": [],
|
|
20873
|
+
"zh": []
|
|
20874
|
+
},
|
|
20875
|
+
"accessibility": {
|
|
20876
|
+
"en": [],
|
|
20877
|
+
"zh": []
|
|
20878
|
+
},
|
|
20879
|
+
"docsUrl": {
|
|
20880
|
+
"en": "https://astryx-vue.pages.dev/en/components/chart",
|
|
20881
|
+
"zh": "https://astryx-vue.pages.dev/components/chart"
|
|
20882
|
+
}
|
|
20883
|
+
},
|
|
19386
20884
|
{
|
|
19387
20885
|
"slug": "badge",
|
|
19388
20886
|
"name": "Badge",
|
|
@@ -37118,6 +38616,604 @@ const catalog = {
|
|
|
37118
38616
|
"zh": "https://astryx-vue.pages.dev/components/power-search"
|
|
37119
38617
|
}
|
|
37120
38618
|
},
|
|
38619
|
+
{
|
|
38620
|
+
"slug": "rich-text-editor",
|
|
38621
|
+
"name": "RichTextEditor",
|
|
38622
|
+
"package": "@astryx-vue/richtext",
|
|
38623
|
+
"kind": "component",
|
|
38624
|
+
"title": {
|
|
38625
|
+
"en": "Rich Text Editor",
|
|
38626
|
+
"zh": "富文本编辑器"
|
|
38627
|
+
},
|
|
38628
|
+
"group": "Form Controls",
|
|
38629
|
+
"groupTitle": {
|
|
38630
|
+
"en": "Form Controls",
|
|
38631
|
+
"zh": "表单控件"
|
|
38632
|
+
},
|
|
38633
|
+
"importPath": "@astryx-vue/richtext",
|
|
38634
|
+
"exportName": "RichTextEditor",
|
|
38635
|
+
"summary": {
|
|
38636
|
+
"en": "Add a formatting toolbar through the #toolbar slot.",
|
|
38637
|
+
"zh": "使用 #toolbar 插槽添加格式工具栏。"
|
|
38638
|
+
},
|
|
38639
|
+
"usage": {
|
|
38640
|
+
"en": "",
|
|
38641
|
+
"zh": ""
|
|
38642
|
+
},
|
|
38643
|
+
"practices": {
|
|
38644
|
+
"en": [],
|
|
38645
|
+
"zh": []
|
|
38646
|
+
},
|
|
38647
|
+
"props": {
|
|
38648
|
+
"en": [
|
|
38649
|
+
{
|
|
38650
|
+
"heading": "`<RichTextEditor>` props",
|
|
38651
|
+
"rows": [
|
|
38652
|
+
{
|
|
38653
|
+
"name": "`label`",
|
|
38654
|
+
"description": "`label`",
|
|
38655
|
+
"type": "`string`",
|
|
38656
|
+
"default": "Required"
|
|
38657
|
+
},
|
|
38658
|
+
{
|
|
38659
|
+
"name": "`isLabelHidden`",
|
|
38660
|
+
"description": "`isLabelHidden`",
|
|
38661
|
+
"type": "`boolean`",
|
|
38662
|
+
"default": "`false`"
|
|
38663
|
+
},
|
|
38664
|
+
{
|
|
38665
|
+
"name": "`description`",
|
|
38666
|
+
"description": "`description`",
|
|
38667
|
+
"type": "`string`",
|
|
38668
|
+
"default": "-"
|
|
38669
|
+
},
|
|
38670
|
+
{
|
|
38671
|
+
"name": "`isOptional`",
|
|
38672
|
+
"description": "`isOptional`",
|
|
38673
|
+
"type": "`boolean`",
|
|
38674
|
+
"default": "`false`"
|
|
38675
|
+
},
|
|
38676
|
+
{
|
|
38677
|
+
"name": "`isRequired`",
|
|
38678
|
+
"description": "`isRequired`",
|
|
38679
|
+
"type": "`boolean`",
|
|
38680
|
+
"default": "`false`"
|
|
38681
|
+
},
|
|
38682
|
+
{
|
|
38683
|
+
"name": "`defaultValue`",
|
|
38684
|
+
"description": "`defaultValue`",
|
|
38685
|
+
"type": "`string`",
|
|
38686
|
+
"default": "-"
|
|
38687
|
+
},
|
|
38688
|
+
{
|
|
38689
|
+
"name": "`placeholder`",
|
|
38690
|
+
"description": "`placeholder`",
|
|
38691
|
+
"type": "`string`",
|
|
38692
|
+
"default": "-"
|
|
38693
|
+
},
|
|
38694
|
+
{
|
|
38695
|
+
"name": "`isReadOnly`",
|
|
38696
|
+
"description": "`isReadOnly`",
|
|
38697
|
+
"type": "`boolean`",
|
|
38698
|
+
"default": "`false`"
|
|
38699
|
+
},
|
|
38700
|
+
{
|
|
38701
|
+
"name": "`isDisabled`",
|
|
38702
|
+
"description": "`isDisabled`",
|
|
38703
|
+
"type": "`boolean`",
|
|
38704
|
+
"default": "`false`"
|
|
38705
|
+
},
|
|
38706
|
+
{
|
|
38707
|
+
"name": "`status`",
|
|
38708
|
+
"description": "`status`",
|
|
38709
|
+
"type": "`RichTextEditorStatus`",
|
|
38710
|
+
"default": "-"
|
|
38711
|
+
},
|
|
38712
|
+
{
|
|
38713
|
+
"name": "`statusVariant`",
|
|
38714
|
+
"description": "`statusVariant`",
|
|
38715
|
+
"type": "`'attached' \\",
|
|
38716
|
+
"default": "'detached' \\"
|
|
38717
|
+
},
|
|
38718
|
+
{
|
|
38719
|
+
"name": "`width`",
|
|
38720
|
+
"description": "`width`",
|
|
38721
|
+
"type": "`number \\",
|
|
38722
|
+
"default": "string`"
|
|
38723
|
+
},
|
|
38724
|
+
{
|
|
38725
|
+
"name": "`minHeight`",
|
|
38726
|
+
"description": "`minHeight`",
|
|
38727
|
+
"type": "`number \\",
|
|
38728
|
+
"default": "string`"
|
|
38729
|
+
},
|
|
38730
|
+
{
|
|
38731
|
+
"name": "`labelTooltip`",
|
|
38732
|
+
"description": "`labelTooltip`",
|
|
38733
|
+
"type": "`string`",
|
|
38734
|
+
"default": "-"
|
|
38735
|
+
},
|
|
38736
|
+
{
|
|
38737
|
+
"name": "`size`",
|
|
38738
|
+
"description": "`size`",
|
|
38739
|
+
"type": "`'sm' \\",
|
|
38740
|
+
"default": "'md' \\"
|
|
38741
|
+
},
|
|
38742
|
+
{
|
|
38743
|
+
"name": "`nodes`",
|
|
38744
|
+
"description": "`nodes`",
|
|
38745
|
+
"type": "`ReadonlyArray>`",
|
|
38746
|
+
"default": "-"
|
|
38747
|
+
},
|
|
38748
|
+
{
|
|
38749
|
+
"name": "`extensions`",
|
|
38750
|
+
"description": "`extensions`",
|
|
38751
|
+
"type": "`ReadonlyArray`",
|
|
38752
|
+
"default": "-"
|
|
38753
|
+
},
|
|
38754
|
+
{
|
|
38755
|
+
"name": "`markdownExtensions`",
|
|
38756
|
+
"description": "`markdownExtensions`",
|
|
38757
|
+
"type": "`ReadonlyArray`",
|
|
38758
|
+
"default": "-"
|
|
38759
|
+
},
|
|
38760
|
+
{
|
|
38761
|
+
"name": "`hasMarkdownShortcuts`",
|
|
38762
|
+
"description": "`hasMarkdownShortcuts`",
|
|
38763
|
+
"type": "`boolean`",
|
|
38764
|
+
"default": "`true`"
|
|
38765
|
+
},
|
|
38766
|
+
{
|
|
38767
|
+
"name": "`transformers`",
|
|
38768
|
+
"description": "`transformers`",
|
|
38769
|
+
"type": "`ReadonlyArray`",
|
|
38770
|
+
"default": "Default transformers"
|
|
38771
|
+
},
|
|
38772
|
+
{
|
|
38773
|
+
"name": "`hasAutoFocus`",
|
|
38774
|
+
"description": "`hasAutoFocus`",
|
|
38775
|
+
"type": "`boolean`",
|
|
38776
|
+
"default": "`false`"
|
|
38777
|
+
},
|
|
38778
|
+
{
|
|
38779
|
+
"name": "`tabEscapeHint`",
|
|
38780
|
+
"description": "`tabEscapeHint`",
|
|
38781
|
+
"type": "`string`",
|
|
38782
|
+
"default": "Localized text"
|
|
38783
|
+
},
|
|
38784
|
+
{
|
|
38785
|
+
"name": "`maxLength`",
|
|
38786
|
+
"description": "`maxLength`",
|
|
38787
|
+
"type": "`number`",
|
|
38788
|
+
"default": "-"
|
|
38789
|
+
},
|
|
38790
|
+
{
|
|
38791
|
+
"name": "`namespace`",
|
|
38792
|
+
"description": "`namespace`",
|
|
38793
|
+
"type": "`string`",
|
|
38794
|
+
"default": "`'astryx-editor'`"
|
|
38795
|
+
}
|
|
38796
|
+
]
|
|
38797
|
+
},
|
|
38798
|
+
{
|
|
38799
|
+
"heading": "`<RichTextEditorToolbar>` props and slots",
|
|
38800
|
+
"rows": [
|
|
38801
|
+
{
|
|
38802
|
+
"name": "`label`",
|
|
38803
|
+
"description": "`label`",
|
|
38804
|
+
"type": "`string`",
|
|
38805
|
+
"default": "Localized text"
|
|
38806
|
+
},
|
|
38807
|
+
{
|
|
38808
|
+
"name": "`headingLevels`",
|
|
38809
|
+
"description": "`headingLevels`",
|
|
38810
|
+
"type": "`ReadonlyArray<'h1' \\",
|
|
38811
|
+
"default": "'h2' \\"
|
|
38812
|
+
},
|
|
38813
|
+
{
|
|
38814
|
+
"name": "`size`",
|
|
38815
|
+
"description": "`size`",
|
|
38816
|
+
"type": "`'sm' \\",
|
|
38817
|
+
"default": "'md' \\"
|
|
38818
|
+
},
|
|
38819
|
+
{
|
|
38820
|
+
"name": "`hasLink`",
|
|
38821
|
+
"description": "`hasLink`",
|
|
38822
|
+
"type": "`boolean`",
|
|
38823
|
+
"default": "`true`"
|
|
38824
|
+
},
|
|
38825
|
+
{
|
|
38826
|
+
"name": "`promptForUrl`",
|
|
38827
|
+
"description": "`promptForUrl`",
|
|
38828
|
+
"type": "`(selectedText: string) => string \\",
|
|
38829
|
+
"default": "null \\"
|
|
38830
|
+
},
|
|
38831
|
+
{
|
|
38832
|
+
"name": "`hasNewTabLinks`",
|
|
38833
|
+
"description": "`hasNewTabLinks`",
|
|
38834
|
+
"type": "`boolean`",
|
|
38835
|
+
"default": "`true`"
|
|
38836
|
+
}
|
|
38837
|
+
]
|
|
38838
|
+
},
|
|
38839
|
+
{
|
|
38840
|
+
"heading": "`<RichTextView>` props, events and slots",
|
|
38841
|
+
"rows": [
|
|
38842
|
+
{
|
|
38843
|
+
"name": "`value`",
|
|
38844
|
+
"description": "`value`",
|
|
38845
|
+
"type": "`string`",
|
|
38846
|
+
"default": "Required"
|
|
38847
|
+
},
|
|
38848
|
+
{
|
|
38849
|
+
"name": "`nodes`",
|
|
38850
|
+
"description": "`nodes`",
|
|
38851
|
+
"type": "`ReadonlyArray>`",
|
|
38852
|
+
"default": "-"
|
|
38853
|
+
},
|
|
38854
|
+
{
|
|
38855
|
+
"name": "`markdownExtensions`",
|
|
38856
|
+
"description": "`markdownExtensions`",
|
|
38857
|
+
"type": "`ReadonlyArray`",
|
|
38858
|
+
"default": "-"
|
|
38859
|
+
},
|
|
38860
|
+
{
|
|
38861
|
+
"name": "`namespace`",
|
|
38862
|
+
"description": "`namespace`",
|
|
38863
|
+
"type": "`string`",
|
|
38864
|
+
"default": "`'astryx-view'`"
|
|
38865
|
+
}
|
|
38866
|
+
]
|
|
38867
|
+
}
|
|
38868
|
+
],
|
|
38869
|
+
"zh": [
|
|
38870
|
+
{
|
|
38871
|
+
"heading": "`<RichTextEditor>` 属性",
|
|
38872
|
+
"rows": [
|
|
38873
|
+
{
|
|
38874
|
+
"name": "`label`",
|
|
38875
|
+
"description": "`label`",
|
|
38876
|
+
"type": "`string`",
|
|
38877
|
+
"default": "必填"
|
|
38878
|
+
},
|
|
38879
|
+
{
|
|
38880
|
+
"name": "`isLabelHidden`",
|
|
38881
|
+
"description": "`isLabelHidden`",
|
|
38882
|
+
"type": "`boolean`",
|
|
38883
|
+
"default": "`false`"
|
|
38884
|
+
},
|
|
38885
|
+
{
|
|
38886
|
+
"name": "`description`",
|
|
38887
|
+
"description": "`description`",
|
|
38888
|
+
"type": "`string`",
|
|
38889
|
+
"default": "-"
|
|
38890
|
+
},
|
|
38891
|
+
{
|
|
38892
|
+
"name": "`isOptional`",
|
|
38893
|
+
"description": "`isOptional`",
|
|
38894
|
+
"type": "`boolean`",
|
|
38895
|
+
"default": "`false`"
|
|
38896
|
+
},
|
|
38897
|
+
{
|
|
38898
|
+
"name": "`isRequired`",
|
|
38899
|
+
"description": "`isRequired`",
|
|
38900
|
+
"type": "`boolean`",
|
|
38901
|
+
"default": "`false`"
|
|
38902
|
+
},
|
|
38903
|
+
{
|
|
38904
|
+
"name": "`defaultValue`",
|
|
38905
|
+
"description": "`defaultValue`",
|
|
38906
|
+
"type": "`string`",
|
|
38907
|
+
"default": "-"
|
|
38908
|
+
},
|
|
38909
|
+
{
|
|
38910
|
+
"name": "`placeholder`",
|
|
38911
|
+
"description": "`placeholder`",
|
|
38912
|
+
"type": "`string`",
|
|
38913
|
+
"default": "-"
|
|
38914
|
+
},
|
|
38915
|
+
{
|
|
38916
|
+
"name": "`isReadOnly`",
|
|
38917
|
+
"description": "`isReadOnly`",
|
|
38918
|
+
"type": "`boolean`",
|
|
38919
|
+
"default": "`false`"
|
|
38920
|
+
},
|
|
38921
|
+
{
|
|
38922
|
+
"name": "`isDisabled`",
|
|
38923
|
+
"description": "`isDisabled`",
|
|
38924
|
+
"type": "`boolean`",
|
|
38925
|
+
"default": "`false`"
|
|
38926
|
+
},
|
|
38927
|
+
{
|
|
38928
|
+
"name": "`status`",
|
|
38929
|
+
"description": "`status`",
|
|
38930
|
+
"type": "`RichTextEditorStatus`",
|
|
38931
|
+
"default": "-"
|
|
38932
|
+
},
|
|
38933
|
+
{
|
|
38934
|
+
"name": "`statusVariant`",
|
|
38935
|
+
"description": "`statusVariant`",
|
|
38936
|
+
"type": "`'attached' \\",
|
|
38937
|
+
"default": "'detached' \\"
|
|
38938
|
+
},
|
|
38939
|
+
{
|
|
38940
|
+
"name": "`width`",
|
|
38941
|
+
"description": "`width`",
|
|
38942
|
+
"type": "`number \\",
|
|
38943
|
+
"default": "string`"
|
|
38944
|
+
},
|
|
38945
|
+
{
|
|
38946
|
+
"name": "`minHeight`",
|
|
38947
|
+
"description": "`minHeight`",
|
|
38948
|
+
"type": "`number \\",
|
|
38949
|
+
"default": "string`"
|
|
38950
|
+
},
|
|
38951
|
+
{
|
|
38952
|
+
"name": "`labelTooltip`",
|
|
38953
|
+
"description": "`labelTooltip`",
|
|
38954
|
+
"type": "`string`",
|
|
38955
|
+
"default": "-"
|
|
38956
|
+
},
|
|
38957
|
+
{
|
|
38958
|
+
"name": "`size`",
|
|
38959
|
+
"description": "`size`",
|
|
38960
|
+
"type": "`'sm' \\",
|
|
38961
|
+
"default": "'md' \\"
|
|
38962
|
+
},
|
|
38963
|
+
{
|
|
38964
|
+
"name": "`nodes`",
|
|
38965
|
+
"description": "`nodes`",
|
|
38966
|
+
"type": "`ReadonlyArray>`",
|
|
38967
|
+
"default": "-"
|
|
38968
|
+
},
|
|
38969
|
+
{
|
|
38970
|
+
"name": "`extensions`",
|
|
38971
|
+
"description": "`extensions`",
|
|
38972
|
+
"type": "`ReadonlyArray`",
|
|
38973
|
+
"default": "-"
|
|
38974
|
+
},
|
|
38975
|
+
{
|
|
38976
|
+
"name": "`markdownExtensions`",
|
|
38977
|
+
"description": "`markdownExtensions`",
|
|
38978
|
+
"type": "`ReadonlyArray`",
|
|
38979
|
+
"default": "-"
|
|
38980
|
+
},
|
|
38981
|
+
{
|
|
38982
|
+
"name": "`hasMarkdownShortcuts`",
|
|
38983
|
+
"description": "`hasMarkdownShortcuts`",
|
|
38984
|
+
"type": "`boolean`",
|
|
38985
|
+
"default": "`true`"
|
|
38986
|
+
},
|
|
38987
|
+
{
|
|
38988
|
+
"name": "`transformers`",
|
|
38989
|
+
"description": "`transformers`",
|
|
38990
|
+
"type": "`ReadonlyArray`",
|
|
38991
|
+
"default": "默认转换器"
|
|
38992
|
+
},
|
|
38993
|
+
{
|
|
38994
|
+
"name": "`hasAutoFocus`",
|
|
38995
|
+
"description": "`hasAutoFocus`",
|
|
38996
|
+
"type": "`boolean`",
|
|
38997
|
+
"default": "`false`"
|
|
38998
|
+
},
|
|
38999
|
+
{
|
|
39000
|
+
"name": "`tabEscapeHint`",
|
|
39001
|
+
"description": "`tabEscapeHint`",
|
|
39002
|
+
"type": "`string`",
|
|
39003
|
+
"default": "本地化文案"
|
|
39004
|
+
},
|
|
39005
|
+
{
|
|
39006
|
+
"name": "`maxLength`",
|
|
39007
|
+
"description": "`maxLength`",
|
|
39008
|
+
"type": "`number`",
|
|
39009
|
+
"default": "-"
|
|
39010
|
+
},
|
|
39011
|
+
{
|
|
39012
|
+
"name": "`namespace`",
|
|
39013
|
+
"description": "`namespace`",
|
|
39014
|
+
"type": "`string`",
|
|
39015
|
+
"default": "`'astryx-editor'`"
|
|
39016
|
+
}
|
|
39017
|
+
]
|
|
39018
|
+
},
|
|
39019
|
+
{
|
|
39020
|
+
"heading": "`<RichTextEditorToolbar>` 属性与插槽",
|
|
39021
|
+
"rows": [
|
|
39022
|
+
{
|
|
39023
|
+
"name": "`label`",
|
|
39024
|
+
"description": "`label`",
|
|
39025
|
+
"type": "`string`",
|
|
39026
|
+
"default": "本地化文案"
|
|
39027
|
+
},
|
|
39028
|
+
{
|
|
39029
|
+
"name": "`headingLevels`",
|
|
39030
|
+
"description": "`headingLevels`",
|
|
39031
|
+
"type": "`ReadonlyArray<'h1' \\",
|
|
39032
|
+
"default": "'h2' \\"
|
|
39033
|
+
},
|
|
39034
|
+
{
|
|
39035
|
+
"name": "`size`",
|
|
39036
|
+
"description": "`size`",
|
|
39037
|
+
"type": "`'sm' \\",
|
|
39038
|
+
"default": "'md' \\"
|
|
39039
|
+
},
|
|
39040
|
+
{
|
|
39041
|
+
"name": "`hasLink`",
|
|
39042
|
+
"description": "`hasLink`",
|
|
39043
|
+
"type": "`boolean`",
|
|
39044
|
+
"default": "`true`"
|
|
39045
|
+
},
|
|
39046
|
+
{
|
|
39047
|
+
"name": "`promptForUrl`",
|
|
39048
|
+
"description": "`promptForUrl`",
|
|
39049
|
+
"type": "`(selectedText: string) => string \\",
|
|
39050
|
+
"default": "null \\"
|
|
39051
|
+
},
|
|
39052
|
+
{
|
|
39053
|
+
"name": "`hasNewTabLinks`",
|
|
39054
|
+
"description": "`hasNewTabLinks`",
|
|
39055
|
+
"type": "`boolean`",
|
|
39056
|
+
"default": "`true`"
|
|
39057
|
+
}
|
|
39058
|
+
]
|
|
39059
|
+
},
|
|
39060
|
+
{
|
|
39061
|
+
"heading": "`<RichTextView>` 属性、事件与插槽",
|
|
39062
|
+
"rows": [
|
|
39063
|
+
{
|
|
39064
|
+
"name": "`value`",
|
|
39065
|
+
"description": "`value`",
|
|
39066
|
+
"type": "`string`",
|
|
39067
|
+
"default": "必填"
|
|
39068
|
+
},
|
|
39069
|
+
{
|
|
39070
|
+
"name": "`nodes`",
|
|
39071
|
+
"description": "`nodes`",
|
|
39072
|
+
"type": "`ReadonlyArray>`",
|
|
39073
|
+
"default": "-"
|
|
39074
|
+
},
|
|
39075
|
+
{
|
|
39076
|
+
"name": "`markdownExtensions`",
|
|
39077
|
+
"description": "`markdownExtensions`",
|
|
39078
|
+
"type": "`ReadonlyArray`",
|
|
39079
|
+
"default": "-"
|
|
39080
|
+
},
|
|
39081
|
+
{
|
|
39082
|
+
"name": "`namespace`",
|
|
39083
|
+
"description": "`namespace`",
|
|
39084
|
+
"type": "`string`",
|
|
39085
|
+
"default": "`'astryx-view'`"
|
|
39086
|
+
}
|
|
39087
|
+
]
|
|
39088
|
+
}
|
|
39089
|
+
]
|
|
39090
|
+
},
|
|
39091
|
+
"slots": {
|
|
39092
|
+
"en": [],
|
|
39093
|
+
"zh": []
|
|
39094
|
+
},
|
|
39095
|
+
"events": {
|
|
39096
|
+
"en": [],
|
|
39097
|
+
"zh": []
|
|
39098
|
+
},
|
|
39099
|
+
"notes": {
|
|
39100
|
+
"en": [
|
|
39101
|
+
{
|
|
39102
|
+
"heading": "Install",
|
|
39103
|
+
"text": "```bash\npnpm add @astryx-vue/richtext\n```\n\n`RichTextEditor` is built on Lexical. Install `lexical` and the `@lexical/*` packages required by the features you use, all pinned to `0.52.0`."
|
|
39104
|
+
},
|
|
39105
|
+
{
|
|
39106
|
+
"heading": "Create an editor",
|
|
39107
|
+
"text": "Import the stylesheet once in your application entry, then render `RichTextEditor`. The required `label` prop provides the editor's accessible name.\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor } from '@astryx-vue/richtext'\nimport '@astryx-vue/richtext/richtext.css'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Publish content\" placeholder=\"Write something...\" />\n</template>\n```\n\nAdd `RichTextEditorToolbar` through the `#toolbar` slot to provide undo, block formats, inline styles, and link actions. The toolbar scrolls horizontally when space is limited."
|
|
39108
|
+
},
|
|
39109
|
+
{
|
|
39110
|
+
"heading": "Save and display content",
|
|
39111
|
+
"text": "The editor and read-only view use the same Lexical state JSON:\n\n1. Listen to the editor's `@change` event and save `editorState.toJSON()` to your backend.\n2. To edit the content again, pass the saved JSON string to `defaultValue`. This prop is read only when the editor mounts.\n3. For read-only display, pass the same string to `RichTextView` through `value` instead of rendering a read-only editor.\n\nWhen you register custom nodes through `nodes`, register the same set on both the editor and the read-only view so content round-trips without data loss."
|
|
39112
|
+
},
|
|
39113
|
+
{
|
|
39114
|
+
"heading": "Keyboard commands",
|
|
39115
|
+
"text": "At the end of a code block or table, <kbd>ArrowDown</kbd> inserts a paragraph when no content follows or when another code block or table immediately follows. This lets you place prose between these blocks. When ordinary content already follows, the arrow keys retain Lexical's native navigation.\n\n- Insert a newline inside a code block: Insert a newline inside a code block\n- Move between table cells: Move between table cells\n- Create and move to a paragraph below a code block or table: Create and move to a paragraph below a code block or table\n- Open or close the link dialog: Open or close the link dialog\n- Leave the editor: Leave the editor"
|
|
39116
|
+
},
|
|
39117
|
+
{
|
|
39118
|
+
"heading": "Automatic links and server-side conversion",
|
|
39119
|
+
"text": "Place `RichTextEditorAutoLinkPlugin` in the editor's default slot to turn typed URLs and email addresses into links. Use `matchers` to add custom matching rules.\n\nTo convert Markdown on the server, import `markdownToEditorStateJSON` and `editorStateJSONToMarkdown` from `@astryx-vue/richtext/markdown`. Neither function requires a browser DOM.\n\nSee the [props documentation](/en/components/rich-text-editor/props) for all properties, events, and slots."
|
|
39120
|
+
},
|
|
39121
|
+
{
|
|
39122
|
+
"heading": "`<RichTextEditor>` props",
|
|
39123
|
+
"text": "- **Events**: `@change=\"(editorState, editor) => void\"` fires when content changes, used for saving state.\n- **Slots**: `#toolbar` top toolbar slot; `#default` internal plugin slot (such as placing `<RichTextEditorAutoLinkPlugin>`).\n- **Template ref methods**: `focus()` focuses; `clear()` clears; `getEditorState()` gets state; `getMarkdown()` exports Markdown; `getHTML()` exports HTML; `getEditor()` gets underlying Lexical instance."
|
|
39124
|
+
},
|
|
39125
|
+
{
|
|
39126
|
+
"heading": "`<RichTextEditorToolbar>` props and slots",
|
|
39127
|
+
"text": "- **Slots**: `#end` custom slot at the end, rendered to the right of default buttons."
|
|
39128
|
+
},
|
|
39129
|
+
{
|
|
39130
|
+
"heading": "`<RichTextView>` props, events and slots",
|
|
39131
|
+
"text": "- **Events**: `@parseError=\"(error: Error) => void\"` fires when parsing content fails.\n- **Slots**: `#errorFallback` fallback content when parsing fails; `#default` internal extension slot."
|
|
39132
|
+
}
|
|
39133
|
+
],
|
|
39134
|
+
"zh": [
|
|
39135
|
+
{
|
|
39136
|
+
"heading": "安装",
|
|
39137
|
+
"text": "```bash\npnpm add @astryx-vue/richtext\n```\n\n`RichTextEditor` 基于 Lexical。你还需要根据所用功能自行安装 `lexical` 和相应的 `@lexical/*` 子包,版本统一为 `0.52.0`。"
|
|
39138
|
+
},
|
|
39139
|
+
{
|
|
39140
|
+
"heading": "创建编辑器",
|
|
39141
|
+
"text": "在应用入口引入一次样式,然后渲染 `RichTextEditor`。`label` 是必填属性,用于标识编辑器的无障碍名称。\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor } from '@astryx-vue/richtext'\nimport '@astryx-vue/richtext/richtext.css'\n<\/script>\n\n<template>\n <RichTextEditor label=\"发布内容\" placeholder=\"请输入正文...\" />\n</template>\n```\n\n通过 `#toolbar` 插槽加入 `RichTextEditorToolbar`,即可获得撤销、块格式、行内样式和链接操作。工具栏空间不足时会横向滚动。"
|
|
39142
|
+
},
|
|
39143
|
+
{
|
|
39144
|
+
"heading": "保存并展示内容",
|
|
39145
|
+
"text": "编辑器和只读视图使用同一份 Lexical 状态 JSON:\n\n1. 监听编辑器的 `@change`,将 `editorState.toJSON()` 保存到后端。\n2. 再次编辑时,把保存的 JSON 字符串传给 `defaultValue`。该属性只在挂载时读取。\n3. 只读展示时,把同一个字符串传给 `RichTextView` 的 `value`,不要再渲染一个只读编辑器。\n\n如果通过 `nodes` 注册自定义节点,编辑器和只读视图必须注册同一组节点,否则内容无法完整往返。"
|
|
39146
|
+
},
|
|
39147
|
+
{
|
|
39148
|
+
"heading": "键盘操作",
|
|
39149
|
+
"text": "光标位于代码块或表格末尾时,<kbd>ArrowDown</kbd> 会在下方没有内容,或者下方紧邻另一个代码块或表格时插入段落。这样可以在这两类块之间补充正文;下方已经是普通内容时,方向键沿用 Lexical 的原生导航。\n\n- 在代码块中换行: 在代码块中换行\n- 在表格单元格之间移动: 在表格单元格之间移动\n- 在代码块或表格末尾创建并进入下方段落: 在代码块或表格末尾创建并进入下方段落\n- 打开或关闭链接对话框: 打开或关闭链接对话框\n- 离开整个编辑器: 离开整个编辑器"
|
|
39150
|
+
},
|
|
39151
|
+
{
|
|
39152
|
+
"heading": "自动链接与服务端转换",
|
|
39153
|
+
"text": "将 `RichTextEditorAutoLinkPlugin` 放入编辑器默认插槽,可以把输入的网址和邮箱自动转换为链接;通过 `matchers` 可以扩展匹配规则。\n\n需要在服务端读写 Markdown 时,从 `@astryx-vue/richtext/markdown` 导入 `markdownToEditorStateJSON` 和 `editorStateJSONToMarkdown`。这两个函数不依赖浏览器 DOM。\n\n完整的属性、事件和插槽见[属性文档](/components/rich-text-editor/props)。"
|
|
39154
|
+
},
|
|
39155
|
+
{
|
|
39156
|
+
"heading": "`<RichTextEditor>` 属性",
|
|
39157
|
+
"text": "- **事件**:`@change=\"(editorState, editor) => void\"` 在内容改变时触发,用于保存状态。\n- **插槽**:`#toolbar` 顶部工具栏插槽;`#default` 内部插件插槽(如放置 `<RichTextEditorAutoLinkPlugin>`)。\n- **模板引用方法**:`focus()` 聚焦;`clear()` 清空;`getEditorState()` 获取状态;`getMarkdown()` 导出 Markdown;`getHTML()` 导出 HTML;`getEditor()` 获取底层 Lexical 实例。"
|
|
39158
|
+
},
|
|
39159
|
+
{
|
|
39160
|
+
"heading": "`<RichTextEditorToolbar>` 属性与插槽",
|
|
39161
|
+
"text": "- **插槽**:`#end` 尾部自定义插槽,渲染在默认按钮右侧。"
|
|
39162
|
+
},
|
|
39163
|
+
{
|
|
39164
|
+
"heading": "`<RichTextView>` 属性、事件与插槽",
|
|
39165
|
+
"text": "- **事件**:`@parseError=\"(error: Error) => void\"` 在内容解析失败时触发。\n- **插槽**:`#errorFallback` 解析失败时的降级显示内容;`#default` 内部扩展插槽。"
|
|
39166
|
+
}
|
|
39167
|
+
]
|
|
39168
|
+
},
|
|
39169
|
+
"examples": [
|
|
39170
|
+
{
|
|
39171
|
+
"title": {
|
|
39172
|
+
"en": "Editor with a toolbar",
|
|
39173
|
+
"zh": "带工具栏的编辑器"
|
|
39174
|
+
},
|
|
39175
|
+
"description": {
|
|
39176
|
+
"en": "Add a formatting toolbar through the #toolbar slot.",
|
|
39177
|
+
"zh": "使用 #toolbar 插槽添加格式工具栏。"
|
|
39178
|
+
},
|
|
39179
|
+
"code": "<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorToolbar } from '@astryx-vue/richtext'\nimport { seed } from './shared'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Release notes\" :default-value=\"seed\" :min-height=\"200\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n </RichTextEditor>\n</template>"
|
|
39180
|
+
},
|
|
39181
|
+
{
|
|
39182
|
+
"title": {
|
|
39183
|
+
"en": "Display saved content",
|
|
39184
|
+
"zh": "只读展示已保存的内容"
|
|
39185
|
+
},
|
|
39186
|
+
"description": {
|
|
39187
|
+
"en": "Render saved Lexical state JSON with RichTextView.",
|
|
39188
|
+
"zh": "将保存的 Lexical 状态 JSON 交给 RichTextView 进行只读渲染。"
|
|
39189
|
+
},
|
|
39190
|
+
"code": "<script setup lang=\"ts\">\nimport { RichTextView } from '@astryx-vue/richtext'\nimport { seed } from './shared'\n<\/script>\n\n<template>\n <RichTextView :value=\"seed\" />\n</template>"
|
|
39191
|
+
},
|
|
39192
|
+
{
|
|
39193
|
+
"title": {
|
|
39194
|
+
"en": "Recognize URLs and email addresses",
|
|
39195
|
+
"zh": "自动识别网址和邮箱"
|
|
39196
|
+
},
|
|
39197
|
+
"description": {
|
|
39198
|
+
"en": "Enable the automatic link plugin in the default slot.",
|
|
39199
|
+
"zh": "在默认插槽中启用自动链接插件。"
|
|
39200
|
+
},
|
|
39201
|
+
"code": "<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorAutoLinkPlugin, RichTextEditorToolbar } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Notes\" placeholder=\"输入网址或邮箱,会自动变成链接\" :min-height=\"120\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n <RichTextEditorAutoLinkPlugin />\n </RichTextEditor>\n</template>"
|
|
39202
|
+
}
|
|
39203
|
+
],
|
|
39204
|
+
"anatomy": {
|
|
39205
|
+
"en": [],
|
|
39206
|
+
"zh": []
|
|
39207
|
+
},
|
|
39208
|
+
"accessibility": {
|
|
39209
|
+
"en": [],
|
|
39210
|
+
"zh": []
|
|
39211
|
+
},
|
|
39212
|
+
"docsUrl": {
|
|
39213
|
+
"en": "https://astryx-vue.pages.dev/en/components/rich-text-editor",
|
|
39214
|
+
"zh": "https://astryx-vue.pages.dev/components/rich-text-editor"
|
|
39215
|
+
}
|
|
39216
|
+
},
|
|
37121
39217
|
{
|
|
37122
39218
|
"slug": "checkbox-indicator",
|
|
37123
39219
|
"name": "CheckboxIndicator",
|
|
@@ -37614,7 +39710,7 @@ const catalog = {
|
|
|
37614
39710
|
},
|
|
37615
39711
|
{
|
|
37616
39712
|
"guidance": "Do",
|
|
37617
|
-
"text": "Use `align=\"
|
|
39713
|
+
"text": "Use `align=\"first-line\"` when a wrapping label sits beside a small mark (a status dot, a radio, a timestamp) that belongs to its first line. Use `align=\"start\"` to line up the tops of a tall slot and the text."
|
|
37618
39714
|
},
|
|
37619
39715
|
{
|
|
37620
39716
|
"guidance": "Do",
|
|
@@ -37660,7 +39756,7 @@ const catalog = {
|
|
|
37660
39756
|
{
|
|
37661
39757
|
"name": "`align`",
|
|
37662
39758
|
"description": "`align`",
|
|
37663
|
-
"type": "`'center' | 'start'`",
|
|
39759
|
+
"type": "`'center' | 'start' | 'first-line'`",
|
|
37664
39760
|
"default": "`'center'`"
|
|
37665
39761
|
},
|
|
37666
39762
|
{
|
|
@@ -37772,9 +39868,9 @@ const catalog = {
|
|
|
37772
39868
|
},
|
|
37773
39869
|
{
|
|
37774
39870
|
"name": "`align`",
|
|
37775
|
-
"type": "`'center' | 'start'`",
|
|
39871
|
+
"type": "`'center' | 'start' | 'first-line'`",
|
|
37776
39872
|
"default": "`'center'`",
|
|
37777
|
-
"description": "
|
|
39873
|
+
"description": "前面和后面那两块内容竖直方向怎么对齐。`center` 把每块对到整块文字的中心;`start` 让插槽顶部和文字顶部对齐;`first-line` 把每块对到标签第一行的中线上,标签换行时小标记也跟第一行走。插槽比这一行高时,文字会被压下去。"
|
|
37778
39874
|
},
|
|
37779
39875
|
{
|
|
37780
39876
|
"name": "`density`",
|
|
@@ -37882,7 +39978,7 @@ const catalog = {
|
|
|
37882
39978
|
"zh": [
|
|
37883
39979
|
{
|
|
37884
39980
|
"heading": "建议",
|
|
37885
|
-
"text": "- 推荐: 常见排版用带名字的插槽(`startContent`、`label`、`description`、`endContent`),八成的场景这样写就够了。\n- 推荐: 菜单和密集列表用 `density=\"compact\"`,普通行用 `\"balanced\"`,宽松布局用 `\"spacious\"`。\n- 推荐: 内容长短不固定时,用 `labelLines` 和 `descriptionLines` 控制在第几行截断。\n- 推荐:
|
|
39981
|
+
"text": "- 推荐: 常见排版用带名字的插槽(`startContent`、`label`、`description`、`endContent`),八成的场景这样写就够了。\n- 推荐: 菜单和密集列表用 `density=\"compact\"`,普通行用 `\"balanced\"`,宽松布局用 `\"spacious\"`。\n- 推荐: 内容长短不固定时,用 `labelLines` 和 `descriptionLines` 控制在第几行截断。\n- 推荐: 换行标签旁边是归属于第一行的小标记(状态点、单选按钮、时间戳)时,用 `align=\"first-line\"`;要让较高的插槽顶部和文字顶部对齐,用 `align=\"start\"`。\n- 推荐: 横滑出来的操作要在行里再留一个入口(菜单,或者 `endContent` 里的按钮)。横滑只是给手指的快捷方式,只能滑出来的操作,键盘和鼠标用户永远够不着。\n- 避免: 不要在可点的 `Item` 里再套按钮或链接。焦点和点击目标会变得难猜。\n- 避免: 不要拿 `Item` 做页面之间的跳转。那种场景用导航组件。\n- 避免: 不要把已读未读、收件箱之类的业务逻辑直接塞进来。包一层薄薄的组件(类似 PreviewItem)更好。"
|
|
37886
39982
|
},
|
|
37887
39983
|
{
|
|
37888
39984
|
"heading": "插槽",
|
|
@@ -37895,6 +39991,17 @@ const catalog = {
|
|
|
37895
39991
|
]
|
|
37896
39992
|
},
|
|
37897
39993
|
"examples": [
|
|
39994
|
+
{
|
|
39995
|
+
"title": {
|
|
39996
|
+
"en": "Align First Line",
|
|
39997
|
+
"zh": "按第一行对齐"
|
|
39998
|
+
},
|
|
39999
|
+
"description": {
|
|
40000
|
+
"en": "Center a small mark on the label's first line when the label wraps, rather than the whole text block. Set align to first-line for a status dot, a radio, or a timestamp that belongs to the first line.",
|
|
40001
|
+
"zh": "标签换行时,把小标记对到标签第一行的中线上,而不是整块文字的中心。状态点、单选按钮、时间戳这类属于第一行的标记用 first-line。"
|
|
40002
|
+
},
|
|
40003
|
+
"code": "<script setup lang=\"ts\">\nimport { Badge } from '@astryx-vue/core/Badge'\nimport { Item } from '@astryx-vue/core/List'\nimport { StatusDot } from '@astryx-vue/core/StatusDot'\nimport { Text } from '@astryx-vue/core/Text'\n<\/script>\n\n<template>\n <div class=\"item-align-first-line\">\n <Item\n align=\"first-line\"\n :label-lines=\"2\"\n label=\"Quarterly planning notes are ready for the team to review\"\n description=\"Agenda, decisions, and follow-ups\"\n >\n <template #startContent>\n <StatusDot label=\"Active\" variant=\"success\" />\n </template>\n <template #endContent>\n <Text color=\"secondary\">\n 12m\n </Text>\n </template>\n </Item>\n <Item\n align=\"first-line\"\n :label-lines=\"2\"\n label=\"Customer research interviews are scheduled for next week\"\n >\n <template #startContent>\n <StatusDot label=\"Away\" variant=\"warning\" />\n </template>\n <template #endContent>\n <Badge label=\"New\" variant=\"blue\" />\n </template>\n </Item>\n <Item\n align=\"first-line\"\n label=\"Launch checklist\"\n description=\"Remaining tasks before release\"\n >\n <template #startContent>\n <StatusDot label=\"Online\" variant=\"success\" />\n </template>\n <template #endContent>\n <Text color=\"secondary\">\n Fri\n </Text>\n </template>\n </Item>\n </div>\n</template>\n\n<style scoped>\n.item-align-first-line {\n display: flex;\n flex-direction: column;\n width: min(100%, 340px);\n min-width: 0;\n}\n</style>"
|
|
40004
|
+
},
|
|
37898
40005
|
{
|
|
37899
40006
|
"title": {
|
|
37900
40007
|
"en": "Basic",
|
|
@@ -46599,7 +48706,7 @@ const catalog = {
|
|
|
46599
48706
|
"name": "`variant`",
|
|
46600
48707
|
"type": "`'pages' | 'count' | 'compact' | 'dots' | 'input' | 'none'`",
|
|
46601
48708
|
"default": "`'pages'`",
|
|
46602
|
-
"description": "Middle of the control. `input` is an editable page field
|
|
48709
|
+
"description": "Middle of the control. `input` is an editable page field; the `compact` readout can add first/last buttons with `hasFirstLast`."
|
|
46603
48710
|
},
|
|
46604
48711
|
{
|
|
46605
48712
|
"name": "`siblingCount`",
|
|
@@ -46628,8 +48735,8 @@ const catalog = {
|
|
|
46628
48735
|
{
|
|
46629
48736
|
"name": "`hasFirstLast`",
|
|
46630
48737
|
"type": "`boolean`",
|
|
46631
|
-
"default": "`
|
|
46632
|
-
"description": "Shows buttons that jump to the first and last page."
|
|
48738
|
+
"default": "`true` for `input`; `false` for `compact`",
|
|
48739
|
+
"description": "Shows buttons that jump to the first and last page. Shown in the `input` variant unless `false`, and in the `compact` variant only when `true`; other variants ignore it. Both are omitted when the page count is unknown."
|
|
46633
48740
|
},
|
|
46634
48741
|
{
|
|
46635
48742
|
"name": "`changeAction`",
|
|
@@ -46724,7 +48831,7 @@ const catalog = {
|
|
|
46724
48831
|
"name": "`variant`",
|
|
46725
48832
|
"type": "`'pages' | 'count' | 'compact' | 'dots' | 'input' | 'none'`",
|
|
46726
48833
|
"default": "`'pages'`",
|
|
46727
|
-
"description": "中间区域。 `input`
|
|
48834
|
+
"description": "中间区域。 `input` 是可输入的页码;`compact` 的「第几页 / 共几页」可以用 `hasFirstLast` 加上首尾按钮。"
|
|
46728
48835
|
},
|
|
46729
48836
|
{
|
|
46730
48837
|
"name": "`siblingCount`",
|
|
@@ -46753,8 +48860,8 @@ const catalog = {
|
|
|
46753
48860
|
{
|
|
46754
48861
|
"name": "`hasFirstLast`",
|
|
46755
48862
|
"type": "`boolean`",
|
|
46756
|
-
"default": "
|
|
46757
|
-
"description": "
|
|
48863
|
+
"default": "`input` 为 `true`;`compact` 为 `false`",
|
|
48864
|
+
"description": "是否在上一页和下一页外侧放跳到首尾的按钮。`input` 形态默认显示(传 `false` 关闭);`compact` 形态只在传 `true` 时显示;其他形态忽略。总页数未知时两个按钮都不显示。"
|
|
46758
48865
|
},
|
|
46759
48866
|
{
|
|
46760
48867
|
"name": "`changeAction`",
|
|
@@ -46882,6 +48989,17 @@ const catalog = {
|
|
|
46882
48989
|
"zh": "输入页码后跳转,两侧可以直达首页和末页。总数未知时改用 hasMore,不再显示末页。"
|
|
46883
48990
|
},
|
|
46884
48991
|
"code": "<script setup lang=\"ts\">\nimport { Pagination } from '@astryx-vue/core/Pagination'\nimport { ref } from 'vue'\n\nconst page = ref(3)\n<\/script>\n\n<template>\n <Pagination\n v-model:page=\"page\"\n :page-size=\"10\"\n :total=\"240\"\n variant=\"input\"\n />\n</template>"
|
|
48992
|
+
},
|
|
48993
|
+
{
|
|
48994
|
+
"title": {
|
|
48995
|
+
"en": "Compact with first and last",
|
|
48996
|
+
"zh": "紧凑分页加首尾"
|
|
48997
|
+
},
|
|
48998
|
+
"description": {
|
|
48999
|
+
"en": "The compact readout can opt into first and last buttons when the page count is known.",
|
|
49000
|
+
"zh": "总数已知时,紧凑分页可以打开首页和末页按钮。"
|
|
49001
|
+
},
|
|
49002
|
+
"code": "<script setup lang=\"ts\">\nimport { Pagination } from '@astryx-vue/core/Pagination'\nimport { ref } from 'vue'\n\nconst page = ref(5)\n<\/script>\n\n<template>\n <Pagination\n v-model:page=\"page\"\n :page-size=\"10\"\n :total=\"100\"\n variant=\"compact\"\n has-first-last\n />\n</template>"
|
|
46885
49003
|
}
|
|
46886
49004
|
],
|
|
46887
49005
|
"anatomy": {
|
|
@@ -57163,8 +59281,8 @@ const catalog = {
|
|
|
57163
59281
|
"zh": "`List` 用来呈现一组彼此相关的条目,`ListItem` 是其中的一行。它渲染出语义正确的 `<ul>` 或 `<ol>`,并统一安排一行内部「起始内容 + 标签 + 描述 + 结束内容」四块位置,所以设置项、通知、步骤说明这类界面不必每次都重新写一遍行布局。"
|
|
57164
59282
|
},
|
|
57165
59283
|
"usage": {
|
|
57166
|
-
"en": "`List` presents a collection of related entries, and `ListItem` is one row inside it. The pair renders a semantic `<ul>` or `<ol>` and lays out every row as start content, label, description, and end content, so a settings screen, an inbox, or a set of steps does not have to rebuild that row layout each time.\n\nOne row needs nothing more than a `label`:\n\n```vue\n<script setup lang=\"ts\">\nimport { List, ListItem } from '@astryx-vue/core/List'\n<\/script>\n\n<template>\n <List header=\"Inbox\">\n <ListItem label=\"Mentions\" description=\"Comments that name you\" />\n <ListItem label=\"Build finished\" description=\"main passed every check\" />\n </List>\n</template>\n```\n\n`description` is the secondary line under the label. The `startContent` and `endContent` slots place an icon, an avatar, a badge, or an action at either end of the row, and `label` and `description` each have a same-named slot for content that needs richer markup than a string.\n\nThe list sets the density for all of its rows, and a single row can override it. `compact` uses 4px of block padding, `balanced` is the 8px default, and `spacious` uses 12px and widens the inline padding by one step as well. `hasDividers` rules a line between neighbouring rows, and the last row never draws one.\n\n`listStyle` decides what appears in front of each row: `disc` draws a filled bullet, `circle` draws an outlined one, and `decimal` switches the list to an `<ol>` and numbers the rows, with `start` choosing the first number. The default `none` draws no marker at all.\n\nA row becomes interactive in one of two ways. Give it `onClick` and a button appears inside the row, filling the content area, so keyboard users can tab to it and press Enter or Space; give it `href` and that same place becomes a link, with `noopener` and `noreferrer` added automatically when `target=\"_blank\"`. Clicking another control inside the row, such as a button in `endContent`, does not run the row's own action.\n\n`isSelected` marks the row as selected. When the row carries a role that permits `aria-selected` — `option`, `tab`, or `row`, for instance — the state is written there; otherwise it falls back to `aria-current=\"true\"`, which is valid on any element, so assistive technology still hears which row is current. `isDisabled` makes the row inert and dims its content.\n\nThe `header` slot renders a title above the list and associates it through `aria-labelledby`, so a screen reader announces the title when it enters the list. `
|
|
57167
|
-
"zh": "`List` 用来呈现一组彼此相关的条目,`ListItem` 是其中的一行。它渲染出语义正确的 `<ul>` 或 `<ol>`,并统一安排一行内部「起始内容 + 标签 + 描述 + 结束内容」四块位置,所以设置项、通知、步骤说明这类界面不必每次都重新写一遍行布局。\n\n一行最少只需要一个 `label`:\n\n```vue\n<script setup lang=\"ts\">\nimport { List, ListItem } from '@astryx-vue/core/List'\n<\/script>\n\n<template>\n <List header=\"Inbox\">\n <ListItem label=\"Mentions\" description=\"Comments that name you\" />\n <ListItem label=\"Build finished\" description=\"main passed every check\" />\n </List>\n</template>\n```\n\n`description` 是标签下方的次要文字。使用 `startContent` 和 `endContent` 两个插槽可以把图标、头像、徽章或操作按钮放在行的两端;`label` 和 `description` 也各有一个同名插槽,当文字需要用富文本表达时改用它即可。\n\n列表的密度由 `List` 的 `density` 统一决定,单行可以用 `ListItem` 的 `density` 覆盖它。`compact` 对应 4 像素的上下内边距,`balanced` 是默认的 8 像素,`spacious` 是 12 像素并把左右内边距也放宽一档。`hasDividers` 会在相邻两行之间画一条分隔线,最后一行不会画。\n\n`listStyle` 决定每行前面画什么标记:`disc` 是实心圆点,`circle` 是空心圆点,`decimal` 会改用 `<ol>` 渲染并给出序号,`start` 用来指定序号从几开始。不需要标记时保持默认的 `none`。\n\n一行可以通过两种方式变成可交互。给出 `onClick` 之后,行内部会出现一个占满内容区的按钮,键盘用户可以用 Tab 聚焦它、用回车或空格触发;给出 `href` 时同一位置会变成链接,`target=\"_blank\"` 会自动补上 `noopener` 和 `noreferrer`。点击行内的其他控件(例如 `endContent` 里的按钮)不会触发这一行自己的动作。\n\n`isSelected` 把行标记为选中。如果这一行带有允许 `aria-selected` 的角色,例如 `option`、`tab` 或 `row`,状态会写进 `aria-selected`;否则会退回到在任何元素上都合法的 `aria-current=\"true\"`,屏幕阅读器仍然能读到当前项。`isDisabled` 会让行变成惰性的,并把它的内容调淡。\n\n`header` 插槽会在列表上方渲染一段标题,并用 `aria-labelledby`
|
|
59284
|
+
"en": "`List` presents a collection of related entries, and `ListItem` is one row inside it. The pair renders a semantic `<ul>` or `<ol>` and lays out every row as start content, label, description, and end content, so a settings screen, an inbox, or a set of steps does not have to rebuild that row layout each time.\n\nOne row needs nothing more than a `label`:\n\n```vue\n<script setup lang=\"ts\">\nimport { List, ListItem } from '@astryx-vue/core/List'\n<\/script>\n\n<template>\n <List header=\"Inbox\">\n <ListItem label=\"Mentions\" description=\"Comments that name you\" />\n <ListItem label=\"Build finished\" description=\"main passed every check\" />\n </List>\n</template>\n```\n\n`description` is the secondary line under the label. The `startContent` and `endContent` slots place an icon, an avatar, a badge, or an action at either end of the row, and `label` and `description` each have a same-named slot for content that needs richer markup than a string.\n\nThe list sets the density for all of its rows, and a single row can override it. `compact` uses 4px of block padding, `balanced` is the 8px default, and `spacious` uses 12px and widens the inline padding by one step as well. `hasDividers` rules a line between neighbouring rows, and the last row never draws one.\n\n`listStyle` decides what appears in front of each row: `disc` draws a filled bullet, `circle` draws an outlined one, and `decimal` switches the list to an `<ol>` and numbers the rows, with `start` choosing the first number. The default `none` draws no marker at all.\n\nA row becomes interactive in one of two ways. Give it `onClick` and a button appears inside the row, filling the content area, so keyboard users can tab to it and press Enter or Space; give it `href` and that same place becomes a link, with `noopener` and `noreferrer` added automatically when `target=\"_blank\"`. Clicking another control inside the row, such as a button in `endContent`, does not run the row's own action.\n\n`isSelected` marks the row as selected. When the row carries a role that permits `aria-selected` — `option`, `tab`, or `row`, for instance — the state is written there; otherwise it falls back to `aria-current=\"true\"`, which is valid on any element, so assistive technology still hears which row is current. `isDisabled` makes the row inert and dims its content.\n\nThe `header` slot renders a title above the list and associates it through `aria-labelledby`, so a screen reader announces the title when it enters the list.\n\n`edge` decides where each row's text, dividers, and hover and selection backgrounds sit relative to the list's edges and the padding of the container around it. The default `padded` keeps the row's own inline padding. `flush` removes it, so text starts on the line of a heading above the list and lines and backgrounds stop at the text. `outset` keeps text on that line and lets lines and backgrounds reach into the container's padding. `bleed` puts text on the container's padding line and runs lines and backgrounds to its edges with square corners, like table rows in a Card. A row's first content — its marker, icon, or avatar, otherwise its label — is what lines up with the heading.\n\n`outset` and `bleed` read the padding published by the nearest container that shares it: a Card, Section, BottomSheet, a padded ScrollableArea, or a Layout region. Place the list directly in that container's padding; padding that Stack, Center, or your own styles add in between is not counted. With no such container, such as a standalone list or one in a Popover, they look like `padded`. `edgeCompensation=\"inline\"` still works as a deprecated alias of `edge=\"outset\"`; set `edge` in new code.\n\nThe package also exports the more general `Item` primitive. `ListItem` is that primitive rendered as an `<li>` with the density and marker from its list; use `Item` directly when the same row layout is needed outside a list, or when you need its extra controls — `as`, `align`, `labelLines`, `descriptionLines`, and `layout`. Most screens only need `List` and `ListItem`.",
|
|
59285
|
+
"zh": "`List` 用来呈现一组彼此相关的条目,`ListItem` 是其中的一行。它渲染出语义正确的 `<ul>` 或 `<ol>`,并统一安排一行内部「起始内容 + 标签 + 描述 + 结束内容」四块位置,所以设置项、通知、步骤说明这类界面不必每次都重新写一遍行布局。\n\n一行最少只需要一个 `label`:\n\n```vue\n<script setup lang=\"ts\">\nimport { List, ListItem } from '@astryx-vue/core/List'\n<\/script>\n\n<template>\n <List header=\"Inbox\">\n <ListItem label=\"Mentions\" description=\"Comments that name you\" />\n <ListItem label=\"Build finished\" description=\"main passed every check\" />\n </List>\n</template>\n```\n\n`description` 是标签下方的次要文字。使用 `startContent` 和 `endContent` 两个插槽可以把图标、头像、徽章或操作按钮放在行的两端;`label` 和 `description` 也各有一个同名插槽,当文字需要用富文本表达时改用它即可。\n\n列表的密度由 `List` 的 `density` 统一决定,单行可以用 `ListItem` 的 `density` 覆盖它。`compact` 对应 4 像素的上下内边距,`balanced` 是默认的 8 像素,`spacious` 是 12 像素并把左右内边距也放宽一档。`hasDividers` 会在相邻两行之间画一条分隔线,最后一行不会画。\n\n`listStyle` 决定每行前面画什么标记:`disc` 是实心圆点,`circle` 是空心圆点,`decimal` 会改用 `<ol>` 渲染并给出序号,`start` 用来指定序号从几开始。不需要标记时保持默认的 `none`。\n\n一行可以通过两种方式变成可交互。给出 `onClick` 之后,行内部会出现一个占满内容区的按钮,键盘用户可以用 Tab 聚焦它、用回车或空格触发;给出 `href` 时同一位置会变成链接,`target=\"_blank\"` 会自动补上 `noopener` 和 `noreferrer`。点击行内的其他控件(例如 `endContent` 里的按钮)不会触发这一行自己的动作。\n\n`isSelected` 把行标记为选中。如果这一行带有允许 `aria-selected` 的角色,例如 `option`、`tab` 或 `row`,状态会写进 `aria-selected`;否则会退回到在任何元素上都合法的 `aria-current=\"true\"`,屏幕阅读器仍然能读到当前项。`isDisabled` 会让行变成惰性的,并把它的内容调淡。\n\n`header` 插槽会在列表上方渲染一段标题,并用 `aria-labelledby` 与列表关联起来,于是屏幕阅读器在进入列表时会先读出这个标题。\n\n`edge` 决定每行文字、分隔线以及悬停和选中背景相对列表边缘与外围容器的位置。默认的 `padded` 保留行自身的内边距。`flush` 去掉这段内边距,文字顶到列表上方标题所在的竖线,分隔线和背景也止于文字。`outset` 同样把文字放在这条线上,让分隔线和背景伸进容器的内边距。`bleed` 让文字落在容器的内边距线上,分隔线和背景一直铺到容器边缘、四角为直角,像 Card 里的表格行。与标题对齐的是行里的第一块内容:有标记、图标或头像时就是它,否则是标签。\n\n`outset` 和 `bleed` 读取最近的、共享内边距的容器公布的数值:Card、Section、BottomSheet、带内边距的 ScrollableArea,或 Layout 区域。列表要直接放在这个容器的内边距里;Stack、Center 或你自己的样式在中间加的内边距不计入。没有这样的容器时(单独的列表、Popover 里的列表),它们和 `padded` 表现一样。`edgeCompensation=\"inline\"` 仍然可用,是 `edge=\"outset\"` 的弃用别名;新代码请写 `edge`。\n\n包里同时导出了更通用的原语 `Item`。`ListItem` 就是它在 `<li>` 上的一层封装,加上来自 `List` 的密度和标记;需要在非列表结构里复用同一套行布局时,可以直接使用 `Item`,它能通过 `as` 决定根元素,并提供 `align`、`labelLines`、`descriptionLines`、`layout` 等额外控制。大多数界面只需要 `List` 和 `ListItem`。"
|
|
57168
59286
|
},
|
|
57169
59287
|
"practices": {
|
|
57170
59288
|
"en": [
|
|
@@ -57184,6 +59302,10 @@ const catalog = {
|
|
|
57184
59302
|
"guidance": "Do",
|
|
57185
59303
|
"text": "Set `descriptionLines` when the length of the content varies, or rely on the default single-line truncation for string descriptions, so the row height does not jump."
|
|
57186
59304
|
},
|
|
59305
|
+
{
|
|
59306
|
+
"guidance": "Do",
|
|
59307
|
+
"text": "Use `edge` to line row text up with a heading above the list: `flush` for static rows, `outset` or `bleed` for rows inside a Card, Section, or BottomSheet."
|
|
59308
|
+
},
|
|
57187
59309
|
{
|
|
57188
59310
|
"guidance": "Don't",
|
|
57189
59311
|
"text": "Nest a second control inside an interactive row as its main action. The row's button already fills the content area, and two click targets make the focus order and the behaviour hard to predict."
|
|
@@ -57214,6 +59336,10 @@ const catalog = {
|
|
|
57214
59336
|
"guidance": "推荐",
|
|
57215
59337
|
"text": "内容长度不定时给出 `descriptionLines`,或者让组件按默认的单行截断来裁剪字符串描述,避免行高随文字跳动。"
|
|
57216
59338
|
},
|
|
59339
|
+
{
|
|
59340
|
+
"guidance": "推荐",
|
|
59341
|
+
"text": "用 `edge` 让行文字与列表上方的标题对齐:静态行用 `flush`,Card、Section 或 BottomSheet 里的行用 `outset` 或 `bleed`。"
|
|
59342
|
+
},
|
|
57217
59343
|
{
|
|
57218
59344
|
"guidance": "避免",
|
|
57219
59345
|
"text": "在可点击的行里再放一个可点击的控件作为主要操作。行的按钮已经占据整块内容区,两个点击目标会让焦点顺序和行为都变得难以预测。"
|
|
@@ -57245,11 +59371,17 @@ const catalog = {
|
|
|
57245
59371
|
"default": "`false`",
|
|
57246
59372
|
"description": "Whether to rule a line between neighbouring rows. The last row never draws one, and rows lose their rounded corners while dividers are on."
|
|
57247
59373
|
},
|
|
59374
|
+
{
|
|
59375
|
+
"name": "`edge`",
|
|
59376
|
+
"type": "`'padded' | 'flush' | 'outset' | 'bleed'`",
|
|
59377
|
+
"default": "`'padded'`",
|
|
59378
|
+
"description": "Where each row's text, dividers, and hover and selection backgrounds sit relative to the list's edges and the padding of the container around it. `padded` keeps the row's own inline padding. `flush` removes it, so text starts on the line of a heading above the list and lines and backgrounds stop at the text. `outset` keeps text on that line and lets lines and backgrounds reach into the container's padding. `bleed` puts text on the container's padding line and runs lines and backgrounds to its edges with square corners, like table rows. A row's first content — its marker, icon, or avatar, otherwise its label — is what lines up with the heading. `outset` and `bleed` read the padding published by the nearest container that shares it (Card, Section, BottomSheet, a padded ScrollableArea, or a Layout region) and look like `padded` when there is none."
|
|
59379
|
+
},
|
|
57248
59380
|
{
|
|
57249
59381
|
"name": "`edgeCompensation`",
|
|
57250
59382
|
"type": "`'inline'`",
|
|
57251
59383
|
"default": "-",
|
|
57252
|
-
"description": "
|
|
59384
|
+
"description": "Deprecated: use `edge=\"outset\"`, which this sets. Still works; `edge` wins when both are set."
|
|
57253
59385
|
},
|
|
57254
59386
|
{
|
|
57255
59387
|
"name": "`listStyle`",
|
|
@@ -57394,11 +59526,17 @@ const catalog = {
|
|
|
57394
59526
|
"default": "`false`",
|
|
57395
59527
|
"description": "是否在相邻两行之间绘制分隔线。最后一行不会绘制,带分隔线时行不再有圆角。"
|
|
57396
59528
|
},
|
|
59529
|
+
{
|
|
59530
|
+
"name": "`edge`",
|
|
59531
|
+
"type": "`'padded' | 'flush' | 'outset' | 'bleed'`",
|
|
59532
|
+
"default": "`'padded'`",
|
|
59533
|
+
"description": "每行文字、分隔线以及悬停和选中背景相对列表边缘与外围容器的位置。`padded` 保留行自身的内边距;`flush` 去掉这段内边距,文字顶到列表上方标题的竖线,分隔线和背景也止于文字;`outset` 把文字放在这条线上,让分隔线和背景伸进容器的内边距;`bleed` 让文字落在容器的内边距线上,分隔线和背景铺到容器边缘、四角为直角,像表格行。与标题对齐的是行里的第一块内容:有标记、图标或头像时就是它,否则是标签。`outset` 和 `bleed` 读取最近的、共享内边距的容器公布的数值(Card、Section、BottomSheet、带内边距的 ScrollableArea,或 Layout 区域);没有这样的容器时和 `padded` 表现一样。"
|
|
59534
|
+
},
|
|
57397
59535
|
{
|
|
57398
59536
|
"name": "`edgeCompensation`",
|
|
57399
59537
|
"type": "`'inline'`",
|
|
57400
59538
|
"default": "-",
|
|
57401
|
-
"description": "
|
|
59539
|
+
"description": "已弃用:请改用 `edge=\"outset\"`,两者位置相同。仍然可用;两者同时设置时以 `edge` 为准。"
|
|
57402
59540
|
},
|
|
57403
59541
|
{
|
|
57404
59542
|
"name": "`listStyle`",
|
|
@@ -57614,7 +59752,7 @@ const catalog = {
|
|
|
57614
59752
|
},
|
|
57615
59753
|
{
|
|
57616
59754
|
"heading": "List",
|
|
57617
|
-
"text": "The root element is a `<ul>`, or an `<ol>` when <code>listStyle=\"decimal\"</code>. A header adds a wrapper element around it.\n\nThe following go to the list element:\n\n- HTML attributes.\n- `aria-*` attributes.\n- `data-*` attributes.\n- `class`.\n- `style`.\n\nThe component writes `role=\"list\"`, `data-density`, `data-dividers`, `data-list-style`, and `data-edge-compensation
|
|
59755
|
+
"text": "The root element is a `<ul>`, or an `<ol>` when <code>listStyle=\"decimal\"</code>. A header adds a wrapper element around it.\n\nThe following go to the list element:\n\n- HTML attributes.\n- `aria-*` attributes.\n- `data-*` attributes.\n- `class`.\n- `style`.\n\nThe component writes `role=\"list\"`, `data-density`, `data-dividers`, `data-list-style`, and `data-edge` (the resolved `padded`, `flush`, `outset`, or `bleed`), plus `data-edge-compensation` while the deprecated prop is set. Without a rendered header an `aria-labelledby` you pass is kept; with one, the list's own header takes over."
|
|
57618
59756
|
},
|
|
57619
59757
|
{
|
|
57620
59758
|
"heading": "ListItem / Item",
|
|
@@ -57632,7 +59770,7 @@ const catalog = {
|
|
|
57632
59770
|
},
|
|
57633
59771
|
{
|
|
57634
59772
|
"heading": "List",
|
|
57635
|
-
"text": "根元素是 `<ul>`,`listStyle=\"decimal\"` 时是 `<ol>`;给出 `header` 时外层会多一个包裹元素。\n\n以下内容传入列表元素:\n\n- HTML 属性。\n- `aria-*` 属性。\n- `data-*` 属性。\n- `class`。\n- `style`。\n\n组件自己写入 `role=\"list\"`、`data-density`、`data-dividers`、`data-list-style` 与 `data-edge-compensation`。未渲染 `header` 时,使用方传入的 `aria-labelledby` 会保留;渲染了 `header` 时由组件的标题接管。"
|
|
59773
|
+
"text": "根元素是 `<ul>`,`listStyle=\"decimal\"` 时是 `<ol>`;给出 `header` 时外层会多一个包裹元素。\n\n以下内容传入列表元素:\n\n- HTML 属性。\n- `aria-*` 属性。\n- `data-*` 属性。\n- `class`。\n- `style`。\n\n组件自己写入 `role=\"list\"`、`data-density`、`data-dividers`、`data-list-style` 与 `data-edge`(解析后的 `padded`、`flush`、`outset` 或 `bleed`);弃用的 prop 有值时还会写入 `data-edge-compensation`。未渲染 `header` 时,使用方传入的 `aria-labelledby` 会保留;渲染了 `header` 时由组件的标题接管。"
|
|
57636
59774
|
},
|
|
57637
59775
|
{
|
|
57638
59776
|
"heading": "ListItem / Item",
|
|
@@ -57695,6 +59833,28 @@ const catalog = {
|
|
|
57695
59833
|
"zh": "触摸横滑露出归档。鼠标不会开始这次拖动,所以同一操作还要留在行里。"
|
|
57696
59834
|
},
|
|
57697
59835
|
"code": "<script setup lang=\"ts\">\nimport { Button } from '@astryx-vue/core/Button'\nimport { List, ListItem } from '@astryx-vue/core/List'\nimport { ref } from 'vue'\n\nconst note = ref('Swipe the row on a touch screen. Archive stays in the row for keyboard and mouse.')\n\nfunction archive(): void {\n note.value = 'Archived.'\n}\n<\/script>\n\n<template>\n <List>\n <ListItem\n label=\"Invoice 1042\"\n description=\"Due Friday\"\n :swipe-actions=\"{ trailing: [{ label: 'Archive', onActivate: archive, variant: 'accent' }] }\"\n >\n <template #endContent>\n <Button label=\"Archive\" size=\"sm\" variant=\"secondary\" @click=\"archive\" />\n </template>\n </ListItem>\n </List>\n <p>{{ note }}</p>\n</template>"
|
|
59836
|
+
},
|
|
59837
|
+
{
|
|
59838
|
+
"title": {
|
|
59839
|
+
"en": "Flush rows",
|
|
59840
|
+
"zh": "标题对齐"
|
|
59841
|
+
},
|
|
59842
|
+
"description": {
|
|
59843
|
+
"en": "Static rows with flush drop their inline padding, so text, dividers, and backgrounds all sit on the heading's line.",
|
|
59844
|
+
"zh": "静态行用 flush 去掉左右内边距,文字、分隔线和背景都落在标题的竖线上。"
|
|
59845
|
+
},
|
|
59846
|
+
"code": "<script setup lang=\"ts\">\nimport { Heading } from '@astryx-vue/core/Heading'\nimport { List, ListItem } from '@astryx-vue/core/List'\n<\/script>\n\n<template>\n <div class=\"list-preview__section\">\n <Heading :level=\"4\">\n Shipping\n </Heading>\n <List edge=\"flush\" has-dividers>\n <ListItem label=\"Standard\" description=\"Arrives in 5–7 days\" />\n <ListItem label=\"Express\" description=\"Arrives tomorrow\" />\n <ListItem label=\"Pickup\" description=\"Ready at the counter\" />\n </List>\n </div>\n</template>"
|
|
59847
|
+
},
|
|
59848
|
+
{
|
|
59849
|
+
"title": {
|
|
59850
|
+
"en": "Edge in a card",
|
|
59851
|
+
"zh": "卡片内的行边缘"
|
|
59852
|
+
},
|
|
59853
|
+
"description": {
|
|
59854
|
+
"en": "Interactive rows in a Card use outset so text lines up with the heading while backgrounds keep their room; bleed runs rows to the card's edges with square corners, like table rows.",
|
|
59855
|
+
"zh": "Card 里的可交互行用 outset 让文字对齐标题、背景保留余量;bleed 则像表格行一样铺到卡片边缘,四角为直角。"
|
|
59856
|
+
},
|
|
59857
|
+
"code": "<script setup lang=\"ts\">\nimport { Card } from '@astryx-vue/core/Card'\nimport { Heading } from '@astryx-vue/core/Heading'\nimport { List, ListItem } from '@astryx-vue/core/List'\nimport { VStack } from '@astryx-vue/core/Stack'\nimport { ref } from 'vue'\n\nconst outsetSelected = ref('mentions')\nconst bleedSelected = ref('mentions')\n<\/script>\n\n<template>\n <div class=\"list-preview__stack\">\n <Card :width=\"360\">\n <VStack :gap=\"2\">\n <Heading :level=\"4\">\n Outset rows\n </Heading>\n <List edge=\"outset\" has-dividers>\n <ListItem\n label=\"Mentions\"\n description=\"Comments that name you\"\n :is-selected=\"outsetSelected === 'mentions'\"\n @click=\"outsetSelected = 'mentions'\"\n />\n <ListItem\n label=\"Follows\"\n description=\"New followers this week\"\n :is-selected=\"outsetSelected === 'follows'\"\n @click=\"outsetSelected = 'follows'\"\n />\n </List>\n </VStack>\n </Card>\n <Card :width=\"360\">\n <VStack :gap=\"2\">\n <Heading :level=\"4\">\n Bleed rows\n </Heading>\n <List edge=\"bleed\" has-dividers>\n <ListItem\n label=\"Mentions\"\n description=\"Comments that name you\"\n :is-selected=\"bleedSelected === 'mentions'\"\n @click=\"bleedSelected = 'mentions'\"\n />\n <ListItem\n label=\"Follows\"\n description=\"New followers this week\"\n :is-selected=\"bleedSelected === 'follows'\"\n @click=\"bleedSelected = 'follows'\"\n />\n </List>\n </VStack>\n </Card>\n </div>\n</template>"
|
|
57698
59858
|
}
|
|
57699
59859
|
],
|
|
57700
59860
|
"anatomy": {
|
|
@@ -74735,217 +76895,6 @@ const catalog = {
|
|
|
74735
76895
|
"zh": "https://astryx-vue.pages.dev/guide/changelog"
|
|
74736
76896
|
}
|
|
74737
76897
|
},
|
|
74738
|
-
{
|
|
74739
|
-
"id": "guide/charts",
|
|
74740
|
-
"kind": "guide",
|
|
74741
|
-
"title": {
|
|
74742
|
-
"en": "Charts",
|
|
74743
|
-
"zh": "图表"
|
|
74744
|
-
},
|
|
74745
|
-
"summary": {
|
|
74746
|
-
"en": "`@astryx-vue/charts` is a lightweight chart component library for Vue. It splits the work in two: d3 handles pure math calculations, such as computing scales and curve paths. Vue templates then take those computed positions and render clean SVG vector elements.",
|
|
74747
|
-
"zh": "`@astryx-vue/charts` 是一个轻量的图表组件库。它把工作分成两半:底层的 d3 只负责纯数学计算(比如算比例尺、算曲线的路径),算好位置后,再由 Vue 模板把内容画成一个个干净的 SVG 矢量图形。"
|
|
74748
|
-
},
|
|
74749
|
-
"headings": {
|
|
74750
|
-
"en": [
|
|
74751
|
-
"What this is",
|
|
74752
|
-
"Install",
|
|
74753
|
-
"Your first chart",
|
|
74754
|
-
"`<Chart>` props and slots",
|
|
74755
|
-
"Props",
|
|
74756
|
-
"Slots",
|
|
74757
|
-
"Series",
|
|
74758
|
-
"Line (`line`)",
|
|
74759
|
-
"Bar (`bar`)",
|
|
74760
|
-
"Area (`area`)",
|
|
74761
|
-
"Dot (`dot`)",
|
|
74762
|
-
"Band (`band`)",
|
|
74763
|
-
"Reference line (`referenceLine`)",
|
|
74764
|
-
"Error bar (`errorBar`)",
|
|
74765
|
-
"Candlestick (`candlestick`)",
|
|
74766
|
-
"Axes, grid, legend, tooltip",
|
|
74767
|
-
"Axes (`ChartAxis`)",
|
|
74768
|
-
"Grid (`ChartGrid`)",
|
|
74769
|
-
"Legend (`ChartLegend`)",
|
|
74770
|
-
"Tooltip (`ChartTooltip`)",
|
|
74771
|
-
"Swatch (`ChartSwatch`)",
|
|
74772
|
-
"Custom tooltip content",
|
|
74773
|
-
"Colors",
|
|
74774
|
-
"Formatting functions",
|
|
74775
|
-
"Accessibility",
|
|
74776
|
-
"Server-side rendering",
|
|
74777
|
-
"Not supported yet"
|
|
74778
|
-
],
|
|
74779
|
-
"zh": [
|
|
74780
|
-
"这是什么",
|
|
74781
|
-
"安装",
|
|
74782
|
-
"第一张图",
|
|
74783
|
-
"`<Chart>` 的属性与插槽",
|
|
74784
|
-
"属性",
|
|
74785
|
-
"插槽",
|
|
74786
|
-
"系列",
|
|
74787
|
-
"折线(line)",
|
|
74788
|
-
"柱状(bar)",
|
|
74789
|
-
"面积(area)",
|
|
74790
|
-
"散点(dot)",
|
|
74791
|
-
"区间带(band)",
|
|
74792
|
-
"参考线(referenceLine)",
|
|
74793
|
-
"误差条(errorBar)",
|
|
74794
|
-
"蜡烛图(candlestick)",
|
|
74795
|
-
"坐标轴、网格、图例、提示框",
|
|
74796
|
-
"坐标轴(ChartAxis)",
|
|
74797
|
-
"网格(ChartGrid)",
|
|
74798
|
-
"图例(ChartLegend)",
|
|
74799
|
-
"提示框(ChartTooltip)",
|
|
74800
|
-
"颜色色块(ChartSwatch)",
|
|
74801
|
-
"自定义提示框内容",
|
|
74802
|
-
"颜色",
|
|
74803
|
-
"格式化函数",
|
|
74804
|
-
"无障碍",
|
|
74805
|
-
"服务端渲染",
|
|
74806
|
-
"现在不支持的"
|
|
74807
|
-
]
|
|
74808
|
-
},
|
|
74809
|
-
"keywords": {
|
|
74810
|
-
"en": [
|
|
74811
|
-
"data",
|
|
74812
|
-
"xKey",
|
|
74813
|
-
"series",
|
|
74814
|
-
"height",
|
|
74815
|
-
"width",
|
|
74816
|
-
"margin",
|
|
74817
|
-
"yBaseline",
|
|
74818
|
-
"yDomain",
|
|
74819
|
-
"xDomain",
|
|
74820
|
-
"legend",
|
|
74821
|
-
"tooltip",
|
|
74822
|
-
"title",
|
|
74823
|
-
"subtitle",
|
|
74824
|
-
"grid",
|
|
74825
|
-
"axes",
|
|
74826
|
-
"interactions",
|
|
74827
|
-
"default",
|
|
74828
|
-
"Option",
|
|
74829
|
-
"color",
|
|
74830
|
-
"curve",
|
|
74831
|
-
"strokeWidth",
|
|
74832
|
-
"dots",
|
|
74833
|
-
"label",
|
|
74834
|
-
"opacity",
|
|
74835
|
-
"radius",
|
|
74836
|
-
"stack",
|
|
74837
|
-
"group",
|
|
74838
|
-
"gradient",
|
|
74839
|
-
"stroke",
|
|
74840
|
-
"dodge",
|
|
74841
|
-
"upper",
|
|
74842
|
-
"lower",
|
|
74843
|
-
"y",
|
|
74844
|
-
"y2",
|
|
74845
|
-
"x",
|
|
74846
|
-
"strokeDasharray",
|
|
74847
|
-
"labelPosition",
|
|
74848
|
-
"bandOpacity",
|
|
74849
|
-
"high",
|
|
74850
|
-
"low",
|
|
74851
|
-
"capWidth",
|
|
74852
|
-
"open",
|
|
74853
|
-
"close",
|
|
74854
|
-
"upColor",
|
|
74855
|
-
"downColor",
|
|
74856
|
-
"Prop",
|
|
74857
|
-
"position",
|
|
74858
|
-
"tickCount",
|
|
74859
|
-
"maxTicks",
|
|
74860
|
-
"tickFormat",
|
|
74861
|
-
"truncate",
|
|
74862
|
-
"animated",
|
|
74863
|
-
"showAxisLine",
|
|
74864
|
-
"showTicks",
|
|
74865
|
-
"horizontal",
|
|
74866
|
-
"vertical",
|
|
74867
|
-
"items",
|
|
74868
|
-
"alignment",
|
|
74869
|
-
"hoverIndicator",
|
|
74870
|
-
"showHoverDots",
|
|
74871
|
-
"placement",
|
|
74872
|
-
"variant"
|
|
74873
|
-
],
|
|
74874
|
-
"zh": [
|
|
74875
|
-
"名称",
|
|
74876
|
-
"data",
|
|
74877
|
-
"xKey",
|
|
74878
|
-
"series",
|
|
74879
|
-
"height",
|
|
74880
|
-
"width",
|
|
74881
|
-
"margin",
|
|
74882
|
-
"yBaseline",
|
|
74883
|
-
"yDomain",
|
|
74884
|
-
"xDomain",
|
|
74885
|
-
"legend",
|
|
74886
|
-
"tooltip",
|
|
74887
|
-
"title",
|
|
74888
|
-
"subtitle",
|
|
74889
|
-
"grid",
|
|
74890
|
-
"axes",
|
|
74891
|
-
"interactions",
|
|
74892
|
-
"default",
|
|
74893
|
-
"选项",
|
|
74894
|
-
"color",
|
|
74895
|
-
"curve",
|
|
74896
|
-
"strokeWidth",
|
|
74897
|
-
"dots",
|
|
74898
|
-
"label",
|
|
74899
|
-
"opacity",
|
|
74900
|
-
"radius",
|
|
74901
|
-
"stack",
|
|
74902
|
-
"group",
|
|
74903
|
-
"gradient",
|
|
74904
|
-
"stroke",
|
|
74905
|
-
"dodge",
|
|
74906
|
-
"upper",
|
|
74907
|
-
"lower",
|
|
74908
|
-
"y",
|
|
74909
|
-
"y2",
|
|
74910
|
-
"x",
|
|
74911
|
-
"strokeDasharray",
|
|
74912
|
-
"labelPosition",
|
|
74913
|
-
"bandOpacity",
|
|
74914
|
-
"high",
|
|
74915
|
-
"low",
|
|
74916
|
-
"capWidth",
|
|
74917
|
-
"open",
|
|
74918
|
-
"close",
|
|
74919
|
-
"upColor",
|
|
74920
|
-
"downColor",
|
|
74921
|
-
"属性",
|
|
74922
|
-
"position",
|
|
74923
|
-
"tickCount",
|
|
74924
|
-
"maxTicks",
|
|
74925
|
-
"tickFormat",
|
|
74926
|
-
"truncate",
|
|
74927
|
-
"animated",
|
|
74928
|
-
"showAxisLine",
|
|
74929
|
-
"showTicks",
|
|
74930
|
-
"horizontal",
|
|
74931
|
-
"vertical",
|
|
74932
|
-
"items",
|
|
74933
|
-
"alignment",
|
|
74934
|
-
"hoverIndicator",
|
|
74935
|
-
"showHoverDots",
|
|
74936
|
-
"placement",
|
|
74937
|
-
"variant"
|
|
74938
|
-
]
|
|
74939
|
-
},
|
|
74940
|
-
"body": {
|
|
74941
|
-
"en": "# Charts\n\n## What this is\n\n`@astryx-vue/charts` is a lightweight chart component library for Vue. It splits the work in two: d3 handles pure math calculations, such as computing scales and curve paths. Vue templates then take those computed positions and render clean SVG vector elements.\n\nBuilding a chart takes two main pieces: an outer `<Chart>` canvas and one or more series. A series is a visual mark on the chart, such as a line, a set of bars, or an area fill. You create a series with a helper function like `line('revenue')`. That tells the chart to read the `revenue` field from each row of data.\n\n## Install\n\nThis package is published under the `canary` tag only. Include the tag when installing:\n\n```bash\npnpm add @astryx-vue/charts@canary\n```\n\n`@astryx-vue/charts` depends on `@astryx-vue/core` and `vue`. Make sure both are installed in your project.\n\nThe library includes base styles. Import the full stylesheet in your entry file:\n\n```ts\nimport '@astryx-vue/charts/charts.css'\n```\n\nTo load styles on demand, import individual files from `@astryx-vue/charts/styles/`, such as `chart.css`, `chart-axis.css`, `chart-grid.css`, `chart-legend.css`, `chart-tooltip.css`, or `chart-swatch.css`.\n\n## Your first chart\n\nA minimal chart requires three props on `<Chart>`:\n- `data`: An array of objects where each item represents one row of data.\n- `xKey`: The field name in each row to use for the horizontal x-axis.\n- `series`: An array of series definitions created with functions such as `line()` or `bar()`.\n\nHere is the shortest working line chart:\n\n```vue\n<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, line } from '@astryx-vue/charts'\n\n// 1. Prepare data\nconst data = [\n { month: 'Jan', revenue: 100, cost: 60 },\n { month: 'Feb', revenue: 150, cost: 80 },\n { month: 'Mar', revenue: 120, cost: 70 },\n]\n\n// 2. Define series: two lines reading revenue and cost\nconst series = [\n line('revenue', { label: 'Revenue' }),\n line('cost', { label: 'Cost' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"data\" x-key=\"month\" :series=\"series\" :height=\"260\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>\n```\n\nHere is the live result:\n\n## `<Chart>` props and slots\n\n`<Chart>` is the root container. It measures container dimensions, builds coordinate scales, calculates geometry positions, and renders the SVG tree.\n\n### Props\n\n| Name | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `data` | `Record<string, unknown>[]` | **Required** | Rows of data to render. Each row contains an x-axis field and one or more series fields. |\n| `xKey` | `string` | **Required** | Field name used as the x value across all series. |\n| `series` | `SeriesDef[]` | **Required** | Array of series definitions, rendered in order on a shared coordinate system. |\n| `height` | `number` | `300` | Chart height in CSS pixels. Non-finite values and negative numbers are treated as 0. |\n| `width` | `number` | `0` | Chart width in CSS pixels. Ignored after the browser first measures the container. Used during server-side rendering when no DOM container exists. |\n| `margin` | `Partial<ChartMargin>` | `{ top: 24, right: 24, bottom: 32, left: 48 }` | Partial margin overrides for the plotting area. Omitted sides use defaults. |\n| `yBaseline` | `'auto' \\| 'zero' \\| 'data'` | `'auto'` | How to determine the y-axis domain when `yDomain` is not set. |\n| `yDomain` | `[number, number]` | `undefined` | Explicit y-axis range `[min, max]`. When set, this range is used directly without baseline adjustments or edge padding. |\n| `xDomain` | `[number, number]` | `undefined` | Explicit range `[min, max]` for numeric x-axes. Stays active even when `data` is empty, keeping streaming windows stable. Ignored on categorical band axes. |\n| `legend` | `boolean \\| ChartLegendConfig` | `false` | Whether to show the legend derived from series, or a configuration object to adjust position and alignment. |\n| `tooltip` | `boolean \\| ChartTooltipConfig` | `false` | Whether to show grouped hover tooltips. |\n| `title` | `string` | `undefined` | Chart title, used as the SVG accessible name. |\n| `subtitle` | `string` | `undefined` | Supporting text below the title, used as the SVG accessible description. |\n\n### Slots\n\n| Name | Bindings | Description |\n| :--- | :--- | :--- |\n| `grid` | None | Background grid rendered behind series, usually containing `<ChartGrid />`. |\n| `axes` | None | Axes rendered behind series, usually containing one or more `<ChartAxis />` components. |\n| `interactions` | None | Custom interaction layer rendered above the pointer capture surface. |\n| `tooltip` | `{ xValue: unknown, rows: unknown[] }` | Custom tooltip slot. `xValue` is the active x value under the pointer. `rows` is the list of series data items for that row. |\n| `default` | None | Extra content rendered after the built-in tooltip layer. |\n\n## Series\n\nA series defines a visual representation of data on the chart. A series object holds internal state during layout and rendering. Because of this, provide each series object to one `<Chart>` only. Create fresh series definitions for each chart instance.\n\nWhen data has missing values (`null`, `undefined`) or non-finite numbers (`NaN`, `Infinity`), line marks break cleanly and leave an empty gap. They never drop to zero or draw false connections across gaps.\n\nWhen `yDomain` is omitted, `yBaseline` controls how the y-axis range is calculated:\n- `'auto'` (default): Marks with a zero baseline (bars, areas) include 0 in the scale. Continuous point marks (lines, dots) keep small padding on both ends so points do not clip at the edges.\n- `'zero'`': Expands the scale symmetrically around 0. Useful for values that swing positive and negative, such as profit and loss.\n- `'data'`: Clamps tightly to the minimum and maximum data values without forcing 0 or adding edge padding.\n\n### Line (`line`)\n\nA line series shows continuous trends over time or across categories.\n\n```ts\ndeclare function line(dataKey: string, options?: LineOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | Auto from theme palette | Line color. |\n| `curve` | `'linear' \\| 'monotone' \\| 'natural' \\| 'step'` | `'monotone'` | Curve interpolation method. Options are linear, monotone cubic spline, natural spline, and step. |\n| `strokeWidth` | `number` | `2` | Line stroke width in pixels. |\n| `dots` | `boolean` | `false` | Whether to render solid circular dots at each data point. |\n| `label` | `string` | `dataKey` | Human-readable name shown in legends and tooltips. |\n\n### Bar (`bar`)\n\nA bar series compares magnitudes across categories or time steps. Bars with the same `group` split the category bandwidth and sit side by side. Bars with the same `stack` stack on top of each other.\n\n```ts\ndeclare function bar(dataKey: string, options?: BarOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `color` | `ColorAccessor` | Auto from theme palette | Bar fill color. Accepts a color string or a function `(datum, index) => string`. |\n| `opacity` | `number` | `1` | Bar opacity, between 0 and 1. |\n| `radius` | `number` | `4` | Corner radius on the bar top in pixels. In stacked bars, only the top segment receives rounded corners. |\n| `stack` | `string` | `undefined` | Stack group name. Series sharing a `stack` accumulate upward from the baseline. |\n| `group` | `string` | `undefined` | Side-by-side group name. Series sharing a `group` sit next to each other within each category. |\n| `label` | `string` | `dataKey` | Human-readable name shown in legends and tooltips. |\n\n### Area (`area`)\n\nAn area series fills the region below a line. It is useful for showing total volume and changing proportions over time. Giving multiple area series the same `stack` name stacks them in order.\n\n```ts\ndeclare function area(dataKey: string, options?: AreaOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | Auto from theme palette | Area fill color. |\n| `opacity` | `number` | `0.3` | Area fill opacity. |\n| `curve` | `'linear' \\| 'monotone' \\| 'natural' \\| 'step'` | `'monotone'` | Curve interpolation algorithm. |\n| `stack` | `string` | `undefined` | Stack group name. Areas sharing a `stack` stack sequentially. |\n| `gradient` | `boolean` | `false` | Whether to apply a vertical gradient fill. |\n| `stroke` | `boolean` | `true` | Whether to draw a stroke line along the top edge of the area. |\n| `label` | `string` | `dataKey` | Human-readable name shown in legends and tooltips. |\n\n### Dot (`dot`)\n\nA dot series shows the distribution of discrete data points.\n\n```ts\ndeclare function dot(dataKey: string, options?: DotOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `color` | `ColorAccessor` | Auto from theme palette | Dot color. Accepts a color string or a per-point accessor function. |\n| `radius` | `number` | `4` | Dot radius in pixels. |\n| `opacity` | `number` | `0.8` | Dot opacity. |\n| `dodge` | `boolean` | `false` | When multiple points share the same x value, offsets them horizontally to reduce overlap. |\n| `label` | `string` | `dataKey` | Human-readable name shown in legends and tooltips. |\n\n### Band (`band`)\n\nA band series fills a translucent area between an upper and a lower boundary field. It is often used to show forecast confidence intervals. Band marks are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function band(options: BandOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `upper` | `string` | **Required** | Field name for the upper boundary value. |\n| `lower` | `string` | **Required** | Field name for the lower boundary value. |\n| `color` | `string` | `'var(--color-accent)'` | Fill color for the band area. |\n| `opacity` | `number` | `0.15` | Fill opacity for the band area. |\n\n### Reference line (`referenceLine`)\n\nA reference line marks a specific horizontal or vertical threshold on the chart, or fills a shaded region between two horizontal values. Reference lines are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function referenceLine(options: ReferenceLineOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `y` | `number` | `undefined` | Draws a horizontal line at this y-axis value. |\n| `y2` | `number` | `undefined` | Second y-axis value. Providing both `y` and `y2` shades a reference band between them. |\n| `x` | `number` | `undefined` | Draws a vertical line at this x-axis value. |\n| `color` | `string` | `'var(--astryx-color-border-emphasized)'` | Line or band fill color. |\n| `strokeWidth` | `number` | `1` | Stroke width in pixels. |\n| `strokeDasharray` | `string` | `undefined` | Dash pattern string, such as `'4 4'`. |\n| `label` | `string` | `undefined` | Text label displayed alongside the line. |\n| `labelPosition` | `'start' \\| 'end'` | `'end'` | Label placement along the line: start/end corresponds to left/right for horizontal lines, and bottom/top for vertical lines. |\n| `bandOpacity` | `number` | `0.1` | Fill opacity for the reference band (active only when `y2` is set). |\n\n### Error bar (`errorBar`)\n\nAn error bar (whisker) displays value uncertainty. It is usually paired with `bar()` or `dot()`. Boundary fields use absolute values, allowing asymmetric error intervals. If a row lacks an upper or lower bound, the chart skips that point instead of drawing misleading lines to zero. Error bars are annotations, so they do not appear in legends or tooltips.\n\n```ts\ndeclare function errorBar(options: ErrorBarOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `high` | `string` | **Required** | Field name for the upper bound (absolute value). |\n| `low` | `string` | **Required** | Field name for the lower bound (absolute value). |\n| `color` | `string` | `'var(--astryx-color-text-primary)'` | Stroke color. |\n| `strokeWidth` | `number` | `1.5` | Stroke width in pixels. |\n| `capWidth` | `number` | `8` | Width in pixels of the horizontal end caps. |\n\n### Candlestick (`candlestick`)\n\nA candlestick series displays open, high, low, and close prices over time, commonly used in financial charts.\n\n```ts\ndeclare function candlestick(options: CandlestickOptions): SeriesDef\n```\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `open` | `string` | **Required** | Field name for the open price. |\n| `high` | `string` | **Required** | Field name for the high price. |\n| `low` | `string` | **Required** | Field name for the low price. |\n| `close` | `string` | **Required** | Field name for the close price. |\n| `upColor` | `string` | `'var(--astryx-color-success)'` | Color for up candles, where close is greater than or equal to open. |\n| `downColor` | `string` | `'var(--astryx-color-error)'` | Color for down candles, where close is lower than open. |\n\n## Axes, grid, legend, tooltip\n\nSupporting chart elements are provided as individual components. You can combine and configure them independently. Setting `legend` or `tooltip` to `true` on `<Chart>` enables their defaults. Pass an object to customize position and layout.\n\n### Axes (`ChartAxis`)\n\nAxes render along chart edges to display ticks and label text.\n\n| Prop | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `position` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | **Required** | Edge to place the axis on. |\n| `tickCount` | `number` | `5` | Desired number of ticks. d3 uses this as a hint to find clean intervals. |\n| `maxTicks` | `number` | `undefined` | Maximum number of tick labels to render. Excess ticks are skipped at uniform intervals. |\n| `tickFormat` | `(value: unknown) => string` | `undefined` | Custom tick label formatting function. |\n| `truncate` | `number` | `undefined` | Maximum characters for tick labels before truncating with an ellipsis. |\n| `animated` | `boolean` | `true` | Whether to smoothly animate tick transitions when positions change. |\n| `showAxisLine` | `boolean` | `true` on bottom axis only | Whether to render the axis baseline. If `showTicks` is true, the baseline is always drawn regardless of position. |\n| `showTicks` | `boolean` | `false` | Whether to draw short perpendicular tick lines at each step. |\n\n### Grid (`ChartGrid`)\n\nGrid lines render behind series elements to help align values visually.\n\n| Prop | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `horizontal` | `boolean` | `true` | Whether to draw horizontal grid lines. |\n| `vertical` | `boolean` | `false` | Whether to draw vertical grid lines. |\n| `tickCount` | `number` | `5` | Approximate number of grid lines. Matching this with `<ChartAxis>` aligns grid lines with axis ticks. |\n\n### Legend (`ChartLegend`)\n\nA legend lists series names alongside their representative colors. Setting `legend` on `<Chart>` shows the legend (off by default), or use the `<ChartLegend>` component directly.\n\n| Prop | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `items` | `LegendItem[]` | `[]` | Explicit array of legend items. Inferred automatically from series when omitted. |\n| `position` | `'top' \\| 'bottom' \\| 'start' \\| 'end'` | `'bottom'` | Docking side relative to the chart canvas. `start` and `end` follow reading direction. |\n| `alignment` | `'start' \\| 'center' \\| 'end'` | `'start'` | Alignment of items along the docked edge. |\n\n### Tooltip (`ChartTooltip`)\n\nA tooltip displays details for the data row under the pointer.\n\n| Prop | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `series` | `readonly SeriesDef[]` | Injected by Chart | Series definitions. When using `<ChartTooltip>` directly, pass the same `series` array. |\n| `hoverIndicator` | `boolean` | `true` | Whether to draw hover indicators. Bars highlight the active column, while lines, areas, and dots draw a vertical rule. |\n| `showHoverDots` | `boolean` | `true` | Whether to highlight active data points with dots (skipped automatically on bars). |\n| `placement` | `'auto' \\| 'right' \\| 'left' \\| 'top'` | `'auto'` | Card placement relative to the active point. `auto` defaults to the right and flips left when room runs out. |\n\n### Swatch (`ChartSwatch`)\n\nA swatch renders a small color marker representing a series, useful inside legends or custom tooltips.\n\n| Prop | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | **Required** | Fill color value. |\n| `variant` | `'square' \\| 'line'` | `'square'` | Swatch shape. `square` renders a small square (for bars); `line` renders a short horizontal line (for lines and areas). |\n\n### Custom tooltip content\n\nWhen the default tooltip layout is not enough, customize the card content through the `#tooltip` slot on `<Chart>`. The slot exposes the active `xValue` and row details in `rows`:\n\n```vue\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" tooltip>\n <template #tooltip=\"{ xValue, rows }\">\n <div class=\"my-custom-tooltip\">\n <strong>{{ xValue }}</strong>\n <div v-for=\"row in asRows(rows)\" :key=\"row.key\">\n <ChartSwatch :color=\"row.color\" variant=\"line\" />\n {{ row.label }}: {{ money(row.value) }}\n </div>\n </div>\n </template>\n </Chart>\n</template>\n```\n\n## Colors\n\nWhen a series does not set an explicit color, the chart assigns one automatically in order from the active theme palette.\n\nAll chart colors use CSS variables underneath. When switching themes or toggling dark mode, chart colors update automatically without re-rendering the SVG tree.\n\nTo inspect or use the current chart palette programmatically, call `useChartColors()`. It returns a shared object across the package:\n\n- `categorical(n: number)`: Returns an array of `n` categorical colors. If `n` exceeds palette size, colors cycle.\n- `sequential`: Sequential color scale helper supporting nine hues (`blue`, `shamrock`, `orange`, `pink`, `purple`, `red`, `teal`, `yellow`, `gray`). Passing a count `n` returns a gradient color array.\n- `diverging`: Two-pole diverging palette helper. Provides `positiveNegative`, `coldHot`, and `custom` (custom hues on each pole).\n- `semantic`: Semantic status colors, containing `positive`, `negative`, `warning`, and `neutral`.\n- `structural`: Structural colors for chart framing, containing `axis`, `grid`, `tick`, and `label`.\n- `alpha(color: string, opacity: number)`: Generates a color with an alpha channel for a given opacity. CSS variables use `color-mix()` automatically.\n\n## Formatting functions\n\nThe library includes formatting helpers for common chart values. Formatting functions follow a safe fallback rule: if an input is `NaN`, `Infinity`, or cannot be parsed, the function returns the original string as-is instead of printing errors or throwing exceptions.\n\n- `compactNumber(value, locale)`: Abbreviates large numbers. For example, `1200` becomes \"1.2K\", `1500000` becomes \"1.5M\", and `1e12` becomes \"1T\".\n- `currency(symbol?, locale?)`: Creates a currency formatter. For example, `currency('$')` can be passed to `ChartAxis`'s `tickFormat`. Values over 1,000 are automatically abbreviated (such as \"$1.5K\").\n- `percent(value, locale)`: Formats a decimal ratio as a percentage. For example, `0.45` becomes \"45%\" and `0.005` becomes \"0.5%\".\n- `shortDate(value, locale)`: Formats dates as a short month and day. For example, \"Jan 5\".\n- `monthYear(value, locale)`: Formats dates as month and year. For example, \"Jan 2024\".\n- `formatList(values, locale)`: Formats a list of words into a natural language string according to locale rules, such as \"A, B, and C\".\n\n## Accessibility\n\nCharts include built-in accessibility features so screen reader users can understand chart content:\n\n- The root element has a `role=\"img\"` attribute.\n- The chart's accessible name prioritizes the `title` prop. When `title` is omitted, the component builds a semantic description from primary series and the x-axis field (such as \"Monthly Revenue and Cost trend\"). If only annotations are present, it falls back to \"Chart\".\n- The `subtitle` prop maps to the chart's accessible description.\n- When total data points (data rows × data columns) do not exceed 100 (`MAX_TABLE_POINTS`), the chart generates a visually hidden data table using `VisuallyHidden`. Screen readers can read exact values row by row. Above 100 points, the table is omitted to prevent excessive speech noise.\n\n## Server-side rendering\n\nDuring server-side rendering (SSR), no browser DOM exists, so the component cannot measure parent container width in pixels.\n\nWhen rendering on the server, pass an explicit `width` prop on `<Chart>`. This allows the server to compute scales and render complete SVG paths and rectangles into HTML. After client-side hydration, once the browser measures the real container width, the chart takes over and responds to container resize events.\n\n## Not supported yet\n\nTo stay lightweight and focused, the package currently leaves out:\n- Four WebGL-based series marks for very large datasets (such as `dotGL`) are not yet included.\n- The Nuxt module does not yet include auto-import switches for `@astryx-vue/charts`. Import components manually when using Nuxt.",
|
|
74942
|
-
"zh": "# 图表\n\n## 这是什么\n\n`@astryx-vue/charts` 是一个轻量的图表组件库。它把工作分成两半:底层的 d3 只负责纯数学计算(比如算比例尺、算曲线的路径),算好位置后,再由 Vue 模板把内容画成一个个干净的 SVG 矢量图形。\n\n画一张图非常直观:外层写一个 `<Chart>` 画布,里面传入数据和一个或多个「系列」。所谓系列,就是图上的一条折线、一组柱子或一片阴影。我们用普通的 JavaScript 函数来创建系列,比如写 `line('revenue')`,图表就知道要读取每行数据里的 `revenue` 字段来画折线。\n\n## 安装\n\n这个包只发布在 `canary` 标签下,安装时需要带上标签:\n\n```bash\npnpm add @astryx-vue/charts@canary\n```\n\n`@astryx-vue/charts` 依赖同级的 `@astryx-vue/core` 和 `vue`,请确保项目中已经安装了它们。\n\n图表内置了基础样式。你可以在入口文件引入完整样式文件:\n\n```ts\nimport '@astryx-vue/charts/charts.css'\n```\n\n如果你想按需加载样式,也可以只引入 `@astryx-vue/charts/styles/` 目录下的某一个样式文件(例如 `chart.css`、`chart-axis.css`、`chart-grid.css`、`chart-legend.css`、`chart-tooltip.css` 或 `chart-swatch.css`)。\n\n## 第一张图\n\n画一张图最少需要给 `<Chart>` 传三个必填属性:\n- `data`:要展示的数据数组,每一项是一行对象。\n- `xKey`:指定每行对象中哪个字段当作横坐标(x 轴)。\n- `series`:标记系列数组,用 `line()`、`bar()` 等函数生成。\n\n下面是一段最短的可运行折线图代码:\n\n```vue\n<script setup lang=\"ts\">\nimport { Chart, ChartAxis, ChartGrid, line } from '@astryx-vue/charts'\n\n// 1. 准备数据\nconst data = [\n { month: '1月', revenue: 100, cost: 60 },\n { month: '2月', revenue: 150, cost: 80 },\n { month: '3月', revenue: 120, cost: 70 },\n]\n\n// 2. 定义系列:画两条线,一条读 revenue,一条读 cost\nconst series = [\n line('revenue', { label: '收入' }),\n line('cost', { label: '成本' }),\n]\n<\/script>\n\n<template>\n <Chart :data=\"data\" x-key=\"month\" :series=\"series\" :height=\"260\" legend tooltip>\n <template #grid>\n <ChartGrid />\n </template>\n <template #axes>\n <ChartAxis position=\"bottom\" />\n <ChartAxis position=\"left\" />\n </template>\n </Chart>\n</template>\n```\n\n实际运行效果如下:\n\n## `<Chart>` 的属性与插槽\n\n`<Chart>` 是整张图表的根容器。它负责测量尺寸、建立坐标系、计算点位并把图形画在 SVG 里。\n\n### 属性\n\n| 名称 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `data` | `Record<string, unknown>[]` | **必填** | 要画的数据行。每行既有 x 对应的列,也有一到多个系列的数据列。 |\n| `xKey` | `string` | **必填** | 每行里当 x 用的字段名,所有系列共用。 |\n| `series` | `SeriesDef[]` | **必填** | 标记定义数组,按顺序画在同一套坐标系上。 |\n| `height` | `number` | `300` | 图表高度(单位是 CSS 像素)。非有限值和负数都会当成 0 处理。 |\n| `width` | `number` | `0` | 图表宽度(单位是 CSS 像素)。浏览器里第一次量到容器宽度后就不再使用;服务端渲染时没有容器可量,全靠它出图。 |\n| `margin` | `Partial<ChartMargin>` | `{ top: 24, right: 24, bottom: 32, left: 48 }` | 绘图区内边距的部分覆盖值,没传的边使用默认留白。 |\n| `yBaseline` | `'auto' \\| 'zero' \\| 'data'` | `'auto'` | 没写 `yDomain` 时 y 轴范围怎么定。 |\n| `yDomain` | `[number, number]` | `undefined` | 显式指定的 y 轴范围 `[min, max]`。一旦指定就以此为准,不额外加基线、也不留上下余量。 |\n| `xDomain` | `[number, number]` | `undefined` | 数值 x 轴的显式范围 `[min, max]`。即使 `data` 为空也会照常生效(保证流式更新窗口稳定)。在离散类目的 band 轴上会被忽略。 |\n| `legend` | `boolean \\| ChartLegendConfig` | `false` | 是否显示从系列推导出的图例,或者传配置对象调整位置与对齐。 |\n| `tooltip` | `boolean \\| ChartTooltipConfig` | `false` | 是否显示分组的悬停提示框。 |\n| `title` | `string` | `undefined` | 图表标题,同时作为 SVG 的无障碍名称(Accessible Name)。 |\n| `subtitle` | `string` | `undefined` | 标题下方的补充说明文字,同时作为 SVG 的无障碍描述。 |\n\n### 插槽\n\n| 名称 | 参数 | 说明 |\n| :--- | :--- | :--- |\n| `grid` | 无 | 画在系列后方的背景网格,通常放置 `<ChartGrid />`。 |\n| `axes` | 无 | 画在系列后方的坐标轴,通常放置若干个 `<ChartAxis />`。 |\n| `interactions` | 无 | 画在指针捕获层上方的自定义交互图层。 |\n| `tooltip` | `{ xValue: unknown, rows: unknown[] }` | 提示框的自定义内容插槽。`xValue` 是当前悬停的 x 坐标值,`rows` 是当前行对应各个系列的数据明细。 |\n| `default` | 无 | 额外扩展内容,画在内置提示框图层之后。 |\n\n## 系列\n\n系列代表图表上展示的具体数据图形。同一个系列对象在布局和渲染时会被写入内部状态,所以同一个系列对象只能提供给一个 `<Chart>` 使用,每张图都要重新创建一份。\n\n如果数据中存在缺失值(`null`、`undefined`)或非有限数字(`NaN`、`Infinity`),折线会自然断开形成空洞,不会强行掉到 0 基线上画出一条错误虚假的连线。\n\n在不设置显式 `yDomain` 时,y 轴的范围由 `yBaseline` 决定:\n- `'auto'`(默认):带零基线的标记(柱状图、面积图)会把 0 算进坐标范围;只有离散连续点的图表(折线图、散点图等)会在上下两侧保留一点余量,防止数据点贴在画布边缘。\n- `'zero'`:以 0 为中心对称展开坐标轴,适合展示正负波动数据(比如盈利与亏损)。\n- `'data'`:紧贴实际数据的最大值与最小值,不强制包含 0,也不留任何边缘余量。\n\n### 折线(line)\n\n折线用于展示数据随时间或类目的连续变化趋势。\n\n```ts\ndeclare function line(dataKey: string, options?: LineOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | 自动取色 | 折线颜色。 |\n| `curve` | `'linear' \\| 'monotone' \\| 'natural' \\| 'step'` | `'monotone'` | 曲线插值方式。分别代表折线、单调三次样条、自然样条和阶梯线。 |\n| `strokeWidth` | `number` | `2` | 线条粗细(像素)。 |\n| `dots` | `boolean` | `false` | 是否在每个数据点上画出实体小圆点。 |\n| `label` | `string` | `dataKey` | 图例和提示框中展示的可读名称。 |\n\n### 柱状(bar)\n\n柱状图用于对比不同分类或时间点下的数值大小。相同 `group` 的柱子会把带宽平分并排展示;相同 `stack` 的柱子会一层层堆叠起来展示。\n\n```ts\ndeclare function bar(dataKey: string, options?: BarOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `color` | `ColorAccessor` | 自动取色 | 柱子填充色,可以是固定颜色字符串,也可以是基于数据项计算颜色的函数 `(datum, index) => string`。 |\n| `opacity` | `number` | `1` | 柱子透明度,取值范围在 0 到 1 之间。 |\n| `radius` | `number` | `4` | 柱子顶部的圆角半径(像素)。堆叠时只有最上面一层有圆角。 |\n| `stack` | `string` | `undefined` | 堆叠分组名称。相同 `stack` 的系列会自底向上累加堆叠。 |\n| `group` | `string` | `undefined` | 并排分组名称。相同 `group` 的系列会在每个类目下并排排列。 |\n| `label` | `string` | `dataKey` | 图例和提示框中展示的可读名称。 |\n\n### 面积(area)\n\n面积图在折线下方填充颜色,非常适合表现总量以及各部分占比的变化。如果给多个面积系列指定相同的 `stack` 名字,它们会逐层叠起来。\n\n```ts\ndeclare function area(dataKey: string, options?: AreaOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | 自动取色 | 面积填充色。 |\n| `opacity` | `number` | `0.3` | 面积填充透明度。 |\n| `curve` | `'linear' \\| 'monotone' \\| 'natural' \\| 'step'` | `'monotone'` | 曲线插值算法。 |\n| `stack` | `string` | `undefined` | 堆叠分组名称。相同 `stack` 的面积会依次堆叠。 |\n| `gradient` | `boolean` | `false` | 是否使用垂直渐变填充。 |\n| `stroke` | `boolean` | `true` | 是否在面积顶部画出边缘描边。 |\n| `label` | `string` | `dataKey` | 图例和提示框中展示的可读名称。 |\n\n### 散点(dot)\n\n散点图用于展示离散数据点的分布情况。\n\n```ts\ndeclare function dot(dataKey: string, options?: DotOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `color` | `ColorAccessor` | 自动取色 | 圆点颜色,支持传函数按点取色。 |\n| `radius` | `number` | `4` | 圆点半径(像素)。 |\n| `opacity` | `number` | `0.8` | 圆点透明度。 |\n| `dodge` | `boolean` | `false` | 当多个点落在同一个 x 坐标时,是否在水平方向上稍微错开位置,避免重叠遮挡。 |\n| `label` | `string` | `dataKey` | 图例和提示框中展示的可读名称。 |\n\n### 区间带(band)\n\n区间带用于在上下两个边界字段之间涂满一层半透明的带状区域,通常用来展示预测区间的波动范围。它属于辅助标注标记,不会出现在图例和悬停提示框中。\n\n```ts\ndeclare function band(options: BandOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `upper` | `string` | **必填** | 上界数据列的字段名。 |\n| `lower` | `string` | **必填** | 下界数据列的字段名。 |\n| `color` | `string` | `'var(--color-accent)'` | 区域填充颜色。 |\n| `opacity` | `number` | `0.15` | 区域透明度。 |\n\n### 参考线(referenceLine)\n\n参考线可以在图表指定的数值位置画一条横线、竖线,或者在两条横线之间涂上一段浅色参考带,常用于标记阈值或目标值。它属于辅助标注标记,不进入图例和提示框。\n\n```ts\ndeclare function referenceLine(options: ReferenceLineOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `y` | `number` | `undefined` | 在指定的 y 轴数值位置画一条水平横线。 |\n| `y2` | `number` | `undefined` | 第二个 y 轴数值。如果同时传入 `y` 和 `y2`,会在两者之间涂上一片参考带。 |\n| `x` | `number` | `undefined` | 在指定的 x 轴数值位置画一条垂直竖线。 |\n| `color` | `string` | `'var(--astryx-color-border-emphasized)'` | 线条或参考带的颜色。 |\n| `strokeWidth` | `number` | `1` | 描边粗细(像素)。 |\n| `strokeDasharray` | `string` | `undefined` | 虚线样式,如 `'4 4'`。 |\n| `label` | `string` | `undefined` | 显示在线条旁边的文字标签内容。 |\n| `labelPosition` | `'start' \\| 'end'` | `'end'` | 标签对齐位置。横线对应左/右端,竖线对应底/顶端。 |\n| `bandOpacity` | `number` | `0.1` | 参考带的填充透明度(仅在同时设置了 `y2` 时生效)。 |\n\n### 误差条(errorBar)\n\n误差条(须线)用于展示数值的不确定性范围。它通常配合 `bar()` 或 `dot()` 一起使用。需要注意的是,误差条传入的上下界字段是绝对数值,因此即使上下误差不对称也可以正确绘制。如果某一行缺失了上界或下界,则会直接跳过该点,绝不绘制误导人的零刻度须线。它属于辅助标注标记,不进入图例和提示框。\n\n```ts\ndeclare function errorBar(options: ErrorBarOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `high` | `string` | **必填** | 上界数据列的字段名(绝对值)。 |\n| `low` | `string` | **必填** | 下界数据列的字段名(绝对值)。 |\n| `color` | `string` | `'var(--astryx-color-text-primary)'` | 描边线条颜色。 |\n| `strokeWidth` | `number` | `1.5` | 线条粗细(像素)。 |\n| `capWidth` | `number` | `8` | 须线两端小横杠的宽度(像素)。 |\n\n### 蜡烛图(candlestick)\n\n蜡烛图(K 线图)用于金融等场景,展示一段时间内的开盘、最高、最低和收盘价。\n\n```ts\ndeclare function candlestick(options: CandlestickOptions): SeriesDef\n```\n\n| 选项 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `open` | `string` | **必填** | 开盘价字段名。 |\n| `high` | `string` | **必填** | 最高价字段名。 |\n| `low` | `string` | **必填** | 最低价字段名。 |\n| `close` | `string` | **必填** | 收盘价字段名。 |\n| `upColor` | `string` | `'var(--astryx-color-success)'` | 阳线(收盘价高于或等于开盘价)的颜色。 |\n| `downColor` | `string` | `'var(--astryx-color-error)'` | 阴线(收盘价低于开盘价)的颜色。 |\n\n## 坐标轴、网格、图例、提示框\n\n图表的辅助元素均以独立组件的形式提供,你可以自由组合和调整。在 `<Chart>` 上直接给 `legend` 或 `tooltip` 传 `true` 即可启用默认样式,也可以传配置对象调整位置。\n\n### 坐标轴(ChartAxis)\n\n坐标轴绘制在图表四周,负责标注刻度与文字。\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `position` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | **必填** | 绘制在哪一条边上。 |\n| `tickCount` | `number` | `5` | 期望显示的刻度数量(该值为参考值,d3 会计算出最工整的步长)。 |\n| `maxTicks` | `number` | `undefined` | 允许显示的最大刻度文字数量,超出时会等间隔跳过。 |\n| `tickFormat` | `(value: unknown) => string` | `undefined` | 自定义刻度文字的格式化函数。 |\n| `truncate` | `number` | `undefined` | 刻度文字最大字符数,超出截断并追加省略号。 |\n| `animated` | `boolean` | `true` | 刻度位置发生变化时是否开启平滑过渡动画。 |\n| `showAxisLine` | `boolean` | 仅底轴为 `true` | 是否绘制轴线。如果设置了 `showTicks`,无论哪条轴都会强制画出轴线。 |\n| `showTicks` | `boolean` | `false` | 是否在每个刻度位置画一小段垂直的短刻度须线。 |\n\n### 网格(ChartGrid)\n\n网格线绘制在数据图形底层,帮助读者对齐数值。\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `horizontal` | `boolean` | `true` | 是否绘制水平网格线。 |\n| `vertical` | `boolean` | `false` | 是否绘制垂直网格线。 |\n| `tickCount` | `number` | `5` | 大约绘制几条网格线。与 `<ChartAxis>` 的 `tickCount` 保持一致可使网格与刻度完全对齐。 |\n\n### 图例(ChartLegend)\n\n图例列出各个系列的名称与代表色。在 `<Chart>` 上写 `legend` 就会显示(默认不显示),也可以单独使用 `<ChartLegend>` 组件。\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `items` | `LegendItem[]` | `[]` | 显式指定的图例项数组。不传则自动从系列推导。 |\n| `position` | `'top' \\| 'bottom' \\| 'start' \\| 'end'` | `'bottom'` | 图例相对图表画布的停靠方位。`start` 和 `end` 会跟随阅读方向。 |\n| `alignment` | `'start' \\| 'center' \\| 'end'` | `'start'` | 图例项在所在边上的对齐方式。 |\n\n### 提示框(ChartTooltip)\n\n鼠标悬停在图表上时,提示框会展示当前位置对应的数据明细。\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `series` | `readonly SeriesDef[]` | 由 Chart 注入 | 系列定义。独立使用该组件时需要传入同一份 `series` 数组。 |\n| `hoverIndicator` | `boolean` | `true` | 悬停时是否绘制辅助指示。柱状图高亮整列,折线、面积和散点绘制垂直指示线。 |\n| `showHoverDots` | `boolean` | `true` | 是否在悬停的数据点上显示高亮小圆点(柱状图自动跳过)。 |\n| `placement` | `'auto' \\| 'right' \\| 'left' \\| 'top'` | `'auto'` | 浮层卡片相对悬停点的位置。`auto` 会优先靠右,放不下时自动翻转到左侧。 |\n\n### 颜色色块(ChartSwatch)\n\n用于在图例或自定义提示框中绘制一小块系列代表色标。\n\n| 属性 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `color` | `string` | **必填** | 填充颜色值。 |\n| `variant` | `'square' \\| 'line'` | `'square'` | 色块形态。`square` 渲染为小方块(适合柱状),`line` 渲染为一小段短线(适合折线和面积)。 |\n\n### 自定义提示框内容\n\n如果内置提示框的样式无法满足要求,可以通过 `<Chart>` 的 `#tooltip` 插槽自定义浮层内容。插槽向外暴露当前悬停的横坐标值 `xValue` 和各系列的数据明细 `rows`:\n\n```vue\n<template>\n <Chart :data=\"monthly\" x-key=\"month\" :series=\"series\" tooltip>\n <template #tooltip=\"{ xValue, rows }\">\n <div class=\"my-custom-tooltip\">\n <strong>{{ xValue }}</strong>\n <div v-for=\"row in asRows(rows)\" :key=\"row.key\">\n <ChartSwatch :color=\"row.color\" variant=\"line\" />\n {{ row.label }}:{{ money(row.value) }}\n </div>\n </div>\n </template>\n </Chart>\n</template>\n```\n\n## 颜色\n\n如果创建系列时没有显式指定颜色,图表会根据主题里的分类色板,按顺序自动给每个系列分配颜色。\n\n所有的图表颜色底层都是基于 CSS 变量实现的。这意味着当页面切换主题风格,或者从浅色模式切换到深色模式时,图表上的颜色会自动跟随环境刷新,不需要重新渲染整张图表。\n\n如果你需要手动获取当前图表体系的配色方案,可以调用 `useChartColors()` 组合式函数。它返回的对象在整个包中全局共享且保持不变,其包含的成员如下:\n\n- `categorical(n: number)`:获取包含指定数量的分类色数组。数量超出槽位时会自动循环。\n- `sequential`:顺序色阶对象,支持 9 种色相(`blue`、`shamrock`、`orange`、`pink`、`purple`、`red`、`teal`、`yellow`、`gray`)。传入数量 `n` 即可返回渐变过渡色数组。\n- `diverging`:双极发散配色。提供 `positiveNegative`(正负分明)、`coldHot`(冷暖分明)以及 `custom`(自定义两侧色相)方法。\n- `semantic`:语义单色值对象,包含 `positive`、`negative`、`warning` 和 `neutral` 四种标准状态色。\n- `structural`:图表骨架的结构色,包含 `axis`(坐标轴线)、`grid`(网格线)、`tick`(刻度短线)和 `label`(文本标签)四种颜色。\n- `alpha(color: string, opacity: number)`:按指定透明度生成带透明通道的颜色值(若为 CSS 变量则自动采用 `color-mix()` 处理)。\n\n## 格式化函数\n\n图表库内置了一组常用的格式化函数。所有格式化函数都遵循安全原则:如果遇到 `NaN`、`Infinity` 或无法解析的值,会原样返回原字符串,绝对不会在界面上打印出异常信息,也不会报错崩溃。\n\n- `compactNumber(value, locale)`:将大数字缩写。例如 `1200` 转为「1.2K」,`1500000` 转为「1.5M」,`1e12` 转为「1T」。\n- `currency(symbol?, locale?)`:创建货币格式化器。例如 `currency('¥')` 可以直接传给 `ChartAxis` 的 `tickFormat` 属性,数值大于一千时会自动应用缩写(如「¥1.5K」)。\n- `percent(value, locale)`:将小数比例转换为百分比。例如 `0.45` 转换为「45%」,`0.005` 转换为「0.5%」。\n- `shortDate(value, locale)`:将日期格式化为简短的月日。例如「Jan 5」或「3月12日」。\n- `monthYear(value, locale)`:将日期格式化为年月。例如「Jan 2024」或「2025年3月」。\n- `formatList(values, locale)`:按指定语言的语法规则把多个词汇拼接为自然语言列表,例如将数组拼成「甲、乙和丙」。\n\n## 无障碍\n\n图表在可访问性方面做了完善的无障碍设计,让视障人士通过读屏软件也能了解图表内容:\n\n- 整个图表根元素具有 `role=\"img\"` 角色属性。\n- 图表的无障碍名称优先使用传入的 `title` 属性;如果没有提供 `title`,系统会根据包含的主要系列与 x 轴字段自动拼接出清晰的语义名称(如「月度 收入和成本 走势」);如果全是辅助标注线,则默认退化为「图表」。\n- 传入的 `subtitle` 会自动关联为图表的无障碍长描述。\n- 当数据点总数(数据行数 × 数据列数)不超过 100 个(即常量 `MAX_TABLE_POINTS`)时,图表会自动生成一份在视觉上完全隐藏的数据表格(通过 `VisuallyHidden` 隐藏在屏幕外)。读屏软件能够直接逐行朗读表格中的精准数据;如果数据量超过 100 点,则不会生成表格以避免造成大量无效的语音噪音。\n\n## 服务端渲染\n\n在服务端渲染(SSR)环境中运行时,由于不存在真实的浏览器 DOM,组件无法自动测量父级容器的实际像素宽度。\n\n因此,在服务端渲染时,必须在 `<Chart>` 上显式传入 `width` 属性指定宽度像素值,服务端才能正确完成比例尺计算并完整输出包含路径和矩形的 SVG 标签。在浏览器端水合后,图表一旦首次测量到真实的容器宽度,便会自动接管并按真实容器尺寸响应式排版。\n\n## 现在不支持的\n\n为了保持包的轻量和聚焦,目前以下能力暂未包含在当前包中:\n- 4 个基于 WebGL 的超大数据集标记(例如 `dotGL` 等标记)尚未收录于此包中。\n- Nuxt 模块中尚未提供针对 `@astryx-vue/charts` 的自动引入配置开关,在 Nuxt 项目中使用时需手动通过 npm 依赖导入。"
|
|
74943
|
-
},
|
|
74944
|
-
"docsUrl": {
|
|
74945
|
-
"en": "https://astryx-vue.pages.dev/en/guide/charts",
|
|
74946
|
-
"zh": "https://astryx-vue.pages.dev/guide/charts"
|
|
74947
|
-
}
|
|
74948
|
-
},
|
|
74949
76898
|
{
|
|
74950
76899
|
"id": "guide/cli",
|
|
74951
76900
|
"kind": "guide",
|
|
@@ -74965,13 +76914,15 @@ const catalog = {
|
|
|
74965
76914
|
"Themes",
|
|
74966
76915
|
"Upgrading and checking a project",
|
|
74967
76916
|
"Flags",
|
|
74968
|
-
"Agent file"
|
|
76917
|
+
"Agent file",
|
|
76918
|
+
"From code"
|
|
74969
76919
|
],
|
|
74970
76920
|
"zh": [
|
|
74971
76921
|
"核心命令",
|
|
74972
76922
|
"页面模板与代码生成",
|
|
74973
76923
|
"常用参数",
|
|
74974
|
-
"配合 AI Agent(生成上下文文件)"
|
|
76924
|
+
"配合 AI Agent(生成上下文文件)",
|
|
76925
|
+
"在代码里调用"
|
|
74975
76926
|
]
|
|
74976
76927
|
},
|
|
74977
76928
|
"keywords": {
|
|
@@ -74979,8 +76930,8 @@ const catalog = {
|
|
|
74979
76930
|
"zh": []
|
|
74980
76931
|
},
|
|
74981
76932
|
"body": {
|
|
74982
|
-
"en": "# CLI\n\n`@astryx-vue/cli` looks up astryx-vue component docs and playground pages from the terminal, for people and for agents. The binary is `astryx-vue`. It is not Meta's `astryx` command, and it does not speak React or StyleX.\n\nInstall it as a dev dependency:\n\n```sh\npnpm add -D @astryx-vue/cli\n```\n\nThe package declares a `bin`, so the command is on `node_modules/.bin` right after installing:\n\n```sh\npnpm exec astryx-vue component Button\nnpx astryx-vue search dialog --json\n```\n\nWithout installing it, run `npx @astryx-vue/cli …`. The scope is required: the bare `astryx-vue` name on npm is not this package.\n\nA script alias is optional, and only useful when an agent shell does not have `node_modules/.bin` on `PATH`:\n\n```json\n{\n \"scripts\": {\n \"astryx-vue\": \"astryx-vue\"\n }\n}\n```\n\n```sh\nnpm run astryx-vue -- component Button\n```\n\n## Commands\n\n`astryx-vue component --list` lists the documented components. `astryx-vue component Button` prints one component's usage, props, slots, events, and examples.\n\n`astryx-vue composable --list` lists the composables, and `astryx-vue composable useClipboard` prints one composable's parameters and return values. Vue calls these functions composables, so the CLI never says hook. `component useClipboard` still finds it and points at the `composable` command.\n\n`astryx-vue docs` lists the guide and theme topics. `astryx-vue docs guide/dark-mode` prints one page's text, and `astryx-vue docs themes/tokens` works too.\n\n`astryx-vue search dialog` ranks components, composables, docs topics, and page templates. `--type` narrows the domain to `component`, `composable`, `doc`, or `template`. `astryx-vue manifest --json` describes the commands, options, response types, error codes, and exit codes. `astryx-vue init --features agents` writes the agent context file.\n\n## Templates\n\n`astryx-vue template --list` lists the 57 playground pages, grouped by category. Every template is a page today; `--type block` answers with a note rather than an error. Each line says what the page needs: a ready template names only `vue` and `@astryx-vue/core`, so copying one leaves no install step.\n\n`astryx-vue template login-card` prints one template's description, the components it uses, the packages it needs, and its source. `--skeleton` prints only the layout skeleton: component tags with layout attributes such as `gap`, `padding`, and `columns`, without text, handlers, or native HTML wrappers.\n\n`astryx-vue template login-card ./src/pages` writes `src/pages/LoginCard.vue`. A path that ends in a file name is the file itself. An existing file is only replaced with `--overwrite` (or `-f`), and a path outside the project is refused. Copying rewrites the playground's demo photo paths to an inline grey placeholder and reports how many it replaced; it does not copy assets and does not install anything, it only names the packages the project is missing.\n\nA few templates import something a project cannot install, such as the playground-only `@astryx-vue/playground-charts` alias. They are marked not ready: you can read them, and copying is refused with the reason.\n\n## Build\n\n`astryx-vue build \"<idea>\"` turns one sentence into a starting point: the closest page template, two alternatives, and the components to use, each with the command to run next. It also lists composables separately.\n\nThe ranking is local and deterministic: keyword matching over each template's slug, title, description, keywords, and components, with rare words weighted higher. There is no network call and no model. Chinese is matched by character pairs, so no spaces are needed.\n\n`astryx-vue build` with no idea prints the workflow: pick a page, copy it, then look up each component before writing code.\n\n## Themes\n\n`astryx-vue theme` lists the built-in themes. `astryx-vue theme butter` prints the four steps that load one: the stylesheet import, the `data-astryx-theme` attribute, how to switch themes or run two side by side, and how to check the result. The steps are this site's own theme pages, so the command and the docs cannot disagree. It writes nothing.\n\n## Upgrading and checking a project\n\n`astryx-vue upgrade` reads the `@astryx-vue/core` version in the project's `package.json`, compares it with the version the CLI documents, and prints the sections of the [upgrade guide](/en/guide/migration) — what changed, how it was written before, how it is written now, what to do. It then looks through the project's own sources for the old names that guide replaces and lists file and line. Nothing is changed, and nothing is fetched from the network.\n\n`astryx-vue doctor` reports five checks and edits nothing: whether core is installed and how its version compares, whether themes is installed, whether `vue` satisfies 3.5.0, whether a theme stylesheet is imported, and whether `data-astryx-theme` is set. Exit code is 0 when everything passed or only warned, and 1 when a check failed.\n\n## Flags\n\n`--json` prints a typed envelope on stdout. `--dense` shortens the text people read; together with `--json` it is ignored, and the envelope stays complete. `--zh` uses Simplified Chinese where this site has a translation. `--cwd` is the project directory `template` and `init` write inside.\n\nThe template `--json` types are `template.list`, `template.show`, `template.skeleton`, and `template.copy`; `build` returns `build.help` and `build.kit`. Errors carry a code such as `ERR_UNKNOWN_TEMPLATE`, `ERR_TEMPLATE_NOT_READY`, `ERR_FILE_EXISTS`, or `ERR_PATH_TRAVERSAL`.\n\n## Agent file\n\n`init` prints the setup guide — which packages to install (a canary package takes `@canary`, once it is on npm), the theme import, the locale snippet, and what to verify — and writes the managed block below. It creates `AGENTS.md` when no agent file exists. Without `--agent`, an agent file the project already has is reused, in the order `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.hermes.md`, `HERMES.md`. `--agent claude` writes `CLAUDE.md`, `hermes` writes `.hermes.md` (or `HERMES.md` when the project already has one), `cursor` and `codex` write `AGENTS.md`, and `all` writes every known file that exists. `--agent-docs-path <file>` writes exactly that file.\n\nThe command owns only the block between these markers:\n\n```html\n<!-- Maintained by astryx-vue, do not edit manually -->\n...\n<!-- astryx-vue block end -->\n```\n\nEvery run replaces that block with freshly generated text. Text outside the markers stays. If the file exists and the markers are missing, the block is appended.\n\nThe block names every package the CLI documents, marks the canary ones, and says plainly when a package is not on npm yet — those get no install command, so nobody is sent to a missing package. Then it describes a three-step workflow: run `build` to pick a page, run `template` to copy it, then run `component`, `composable`, or `docs` before writing code. It also says how to invoke the binary.\n\nThe install command follows the project's package manager: the `packageManager` field first, then a lockfile. When neither says which manager it is, all four forms are printed.\n\nThe sentences the CLI prints come from this docs site and from the playground pages. After a docs or template change, regenerate the catalog in the CLI package. The maintainer steps are in the `
|
|
74983
|
-
"zh": "# 命令行\n\n`@astryx-vue/cli` 是一个轻巧的命令行工具,用来在终端里随时查 astryx-vue 的组件文档和演练场页面,方便开发者和 AI Agent 使用。二进制命令名是 `astryx-vue`(请注意:它专为本项目服务,并非 Meta 原版的 `astryx` 命令,也不提供 React 或 StyleX 的功能)。\n\n建议在项目中作为开发依赖安装:\n\n```sh\npnpm add -D @astryx-vue/cli\n```\n\n包里声明了 `bin`,装完命令就在 `node_modules/.bin` 里,直接就能用:\n\n```sh\npnpm exec astryx-vue component Button\nnpx astryx-vue search dialog --json\n```\n\n不安装、只想临时用时,运行 `npx @astryx-vue/cli …`。注意 scope 不能省略,因为 npm 上裸名 `astryx-vue` 并不是这个包。\n\n脚本别名是可选项,只有当 agent 的环境没有把 `node_modules/.bin` 放进 `PATH` 时才需要在 `package.json` 里添加:\n\n```json\n{\n \"scripts\": {\n \"astryx-vue\": \"astryx-vue\"\n }\n}\n```\n\n配置后即可在终端运行:\n\n```sh\nnpm run astryx-vue -- component Button\n```\n\n## 核心命令\n\n- `astryx-vue component --list`:列出当前所有可用组件。\n- `astryx-vue component Button`:快速查看指定组件的基本用法、Props 属性、插槽、事件以及代码示例。\n- `astryx-vue composable --list`:列出组合式函数,`astryx-vue composable useClipboard` 打印其参数和返回值。Vue 里这类函数叫组合式函数(composable),所以命令与文档中统一不使用 hook 这个词。若误输入 `component useClipboard` 仍然能查到,并会友好提示改用 `composable` 命令。\n- `astryx-vue docs`:列出指南与主题文档。`astryx-vue docs guide/dark-mode` 或 `astryx-vue docs themes/tokens` 可直接在终端阅读正文。\n- `astryx-vue search dialog`:在组件、组合式函数、文档主题和页面模板里按相关度搜索。`--type` 可以限定为 `component`、`composable`、`doc` 或 `template`。\n- `astryx-vue manifest --json`:输出 CLI 自身的接口定义清单(命令、选项、响应结构、错误码及退出码),方便被上层工具或脚本消费。\n- `astryx-vue theme`:列出全部内置主题;`astryx-vue theme butter` 输出这套主题的接入四步(引哪个样式表、`data-astryx-theme` 写什么、怎么换主题或同时用两套、改完怎么验证)。\n- `astryx-vue upgrade`:读项目里 `@astryx-vue/core` 的版本,和这个 CLI 对应版本比较,打印[升级指南](/guide/migration)里要做的每一件事,再在项目源码里指出旧名字所在文件和行号。不联网,不改文件。\n- `astryx-vue doctor`:检查依赖装没装、版本对不对、主题样式引没引、主题属性设没设,只报告不改文件;有失败时退出码是 1。\n- `astryx-vue init`:打印给 AI 助手的接入说明(装哪些包、主题样式引哪一行、语言怎么提供),并在项目根目录维护 agent 上下文文件。\n\n## 页面模板与代码生成\n\n- `astryx-vue template --list`:列出 57 个演练场页面模板,按分类分组。目前所有模板都是页面;`--type block` 会给出一句说明,不报错。每行还会说明这个页面要什么:可复制的模板只依赖 `vue` 和 `@astryx-vue/core`,落地后不用再装别的包。\n- `astryx-vue template login-card`:打印一个模板的说明、用到的组件、要装的包和完整源码。`--skeleton` 只打印布局骨架(组件标签加上 `gap`、`padding`、`columns` 这类布局属性,不含文字、事件处理和原生 HTML 包裹层)。\n- `astryx-vue template login-card ./src/pages`:将模板代码生成并写出到 `src/pages/LoginCard.vue`。如果文件已存在,需要加上 `--overwrite`(或 `-f`)才会覆盖,避免误冲掉已有代码。复制时会自动把演示图片替换为内联的灰色占位图,不依赖额外的静态图片资源。\n- `astryx-vue build \"<想法>\"`:把一句话需求变成开发起点,在本地匹配出最接近的页面模板、备选方案以及需要搭配使用的组件与函数。打分完全在本地完成,无网络开销也无需调用外部大模型。\n\n## 常用参数\n\n- `--json`:以 JSON 格式输出带类型的结构化信封数据,适合被工具或脚本解析。模板相关的类型包括 `template.list`、`template.show`、`template.skeleton`、`template.copy`;`build` 返回 `build.help` 和 `build.kit`。出错时信封里带有错误码(如 `ERR_UNKNOWN_TEMPLATE`、`ERR_FILE_EXISTS` 等)。\n- `--dense`:精简输出文本,降低阅读噪音;与 `--json` 配合使用时会被自动忽略,保持 JSON 结构完整。\n- `--zh`:当文档具备中文翻译时,优先输出简体中文内容。\n- `--cwd`:指定项目目录,`template` 和 `init` 只往这个目录下写文件,`upgrade` 和 `doctor` 只在这个目录里读。\n\n## 配合 AI Agent(生成上下文文件)\n\n项目里没有任何 agent 文件时,`init` 默认创建 `AGENTS.md`。不加 `--agent` 时会优先写项目里已有的配置文件,检测顺序为 `AGENTS.md`、`CLAUDE.md`、`.cursorrules`、`.hermes.md`、`HERMES.md`。`--agent claude` 写 `CLAUDE.md`,`hermes` 写 `.hermes.md`,`cursor` 和 `codex` 写 `AGENTS.md`,`all` 则写所有已存在的已知文件。通过 `--agent-docs-path <file>` 也可以指定输出到任意文件。\n\n命令行工具在维护这些文件时,只会在以下固定的起止标记之间更新内容:\n\n```html\n<!-- Maintained by astryx-vue, do not edit manually -->\n...\n<!-- astryx-vue block end -->\n```\n\n区块开头列出这个 CLI 覆盖的每个包,能发 canary 的标出来;npm 上还没有的包会直说「还没发布到 npm」,并且不给安装命令,免得有人照着敲一个装不到的包名。随后是三步工作流:用 `build` 挑页面,用 `template` 复制,写代码前用 `component`、`composable` 或 `docs` 查询用法。\n\n`init` 打印的安装命令跟着项目走:先看 `packageManager` 字段,再看锁文件;两样都没有时把四种写法都列出来。\n\n每次运行命令,工具只会用最新的组件库信息替换这两个标记之间的内容,标记之外由你自己撰写的项目规则会完好无损地保留下来。如果文件已存在但尚未包含这些标记,工具会安全地将其追加到文件末尾。\n\n> **维护者提醒**:CLI 打印的内容均提取自当前文档站和演练场页面。当文档或模板修改后,需要重新生成 CLI 包内置的目录快照,操作步骤记录在 `packages/cli`
|
|
76933
|
+
"en": "# CLI\n\n`@astryx-vue/cli` looks up astryx-vue component docs and playground pages from the terminal, for people and for agents. The binary is `astryx-vue`. It is not Meta's `astryx` command, and it does not speak React or StyleX.\n\nInstall it as a dev dependency:\n\n```sh\npnpm add -D @astryx-vue/cli\n```\n\nThe package declares a `bin`, so the command is on `node_modules/.bin` right after installing:\n\n```sh\npnpm exec astryx-vue component Button\nnpx astryx-vue search dialog --json\n```\n\nWithout installing it, run `npx @astryx-vue/cli …`. The scope is required: the bare `astryx-vue` name on npm is not this package.\n\nA script alias is optional, and only useful when an agent shell does not have `node_modules/.bin` on `PATH`:\n\n```json\n{\n \"scripts\": {\n \"astryx-vue\": \"astryx-vue\"\n }\n}\n```\n\n```sh\nnpm run astryx-vue -- component Button\n```\n\n## Commands\n\n`astryx-vue component --list` lists the documented components. `astryx-vue component Button` prints one component's usage, props, slots, events, and examples.\n\n`astryx-vue composable --list` lists the composables, and `astryx-vue composable useClipboard` prints one composable's parameters and return values. Vue calls these functions composables, so the CLI never says hook. `component useClipboard` still finds it and points at the `composable` command.\n\n`astryx-vue docs` lists the guide and theme topics. `astryx-vue docs guide/dark-mode` prints one page's text, and `astryx-vue docs themes/tokens` works too.\n\n`astryx-vue search dialog` ranks components, composables, docs topics, and page templates. `--type` narrows the domain to `component`, `composable`, `doc`, or `template`. `astryx-vue manifest --json` describes the commands, options, response types, error codes, and exit codes. `astryx-vue init --features agents` writes the agent context file.\n\n## Templates\n\n`astryx-vue template --list` lists the 57 playground pages, grouped by category. Every template is a page today; `--type block` answers with a note rather than an error. Each line says what the page needs: a ready template names only `vue` and `@astryx-vue/core`, so copying one leaves no install step.\n\n`astryx-vue template login-card` prints one template's description, the components it uses, the packages it needs, and its source. `--skeleton` prints only the layout skeleton: component tags with layout attributes such as `gap`, `padding`, and `columns`, without text, handlers, or native HTML wrappers.\n\n`astryx-vue template login-card ./src/pages` writes `src/pages/LoginCard.vue`. A path that ends in a file name is the file itself. An existing file is only replaced with `--overwrite` (or `-f`), and a path outside the project is refused. Copying rewrites the playground's demo photo paths to an inline grey placeholder and reports how many it replaced; it does not copy assets and does not install anything, it only names the packages the project is missing.\n\nA few templates import something a project cannot install, such as the playground-only `@astryx-vue/playground-charts` alias. They are marked not ready: you can read them, and copying is refused with the reason.\n\n## Build\n\n`astryx-vue build \"<idea>\"` turns one sentence into a starting point: the closest page template, two alternatives, and the components to use, each with the command to run next. It also lists composables separately.\n\nThe ranking is local and deterministic: keyword matching over each template's slug, title, description, keywords, and components, with rare words weighted higher. There is no network call and no model. Chinese is matched by character pairs, so no spaces are needed.\n\n`astryx-vue build` with no idea prints the workflow: pick a page, copy it, then look up each component before writing code.\n\n## Themes\n\n`astryx-vue theme` lists the built-in themes. `astryx-vue theme butter` prints the four steps that load one: the stylesheet import, the `data-astryx-theme` attribute, how to switch themes or run two side by side, and how to check the result. The steps are this site's own theme pages, so the command and the docs cannot disagree. It writes nothing.\n\n## Upgrading and checking a project\n\n`astryx-vue upgrade` reads the `@astryx-vue/core` version in the project's `package.json`, compares it with the version the CLI documents, and prints the sections of the [upgrade guide](/en/guide/migration) — what changed, how it was written before, how it is written now, what to do. It then looks through the project's own sources for the old names that guide replaces and lists file and line. Nothing is changed, and nothing is fetched from the network.\n\n`astryx-vue doctor` reports five checks and edits nothing: whether core is installed and how its version compares, whether themes is installed, whether `vue` satisfies 3.5.0, whether a theme stylesheet is imported, and whether `data-astryx-theme` is set. Exit code is 0 when everything passed or only warned, and 1 when a check failed.\n\n## Flags\n\n`--json` prints a typed envelope on stdout. `--dense` shortens the text people read; together with `--json` it is ignored, and the envelope stays complete. `--zh` uses Simplified Chinese where this site has a translation. `--cwd` is the project directory `template` and `init` write inside.\n\nThe template `--json` types are `template.list`, `template.show`, `template.skeleton`, and `template.copy`; `build` returns `build.help` and `build.kit`. Errors carry a code such as `ERR_UNKNOWN_TEMPLATE`, `ERR_TEMPLATE_NOT_READY`, `ERR_FILE_EXISTS`, or `ERR_PATH_TRAVERSAL`.\n\n## Agent file\n\n`init` prints the setup guide — which packages to install (a canary package takes `@canary`, once it is on npm), the theme import, the locale snippet, and what to verify — and writes the managed block below. It creates `AGENTS.md` when no agent file exists. Without `--agent`, an agent file the project already has is reused, in the order `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.hermes.md`, `HERMES.md`. `--agent claude` writes `CLAUDE.md`, `hermes` writes `.hermes.md` (or `HERMES.md` when the project already has one), `cursor` and `codex` write `AGENTS.md`, and `all` writes every known file that exists. `--agent-docs-path <file>` writes exactly that file.\n\nThe command owns only the block between these markers:\n\n```html\n<!-- Maintained by astryx-vue, do not edit manually -->\n...\n<!-- astryx-vue block end -->\n```\n\nEvery run replaces that block with freshly generated text. Text outside the markers stays. If the file exists and the markers are missing, the block is appended.\n\nThe block names every package the CLI documents, marks the canary ones, and says plainly when a package is not on npm yet — those get no install command, so nobody is sent to a missing package. Then it describes a three-step workflow: run `build` to pick a page, run `template` to copy it, then run `component`, `composable`, or `docs` before writing code. It also says how to invoke the binary.\n\nThe install command follows the project's package manager: the `packageManager` field first, then a lockfile. When neither says which manager it is, all four forms are printed.\n\nThe sentences the CLI prints come from this docs site and from the playground pages. After a docs or template change, regenerate the catalog in the CLI package. The maintainer steps are in `packages/cli/CONTRIBUTING.md`.\n\n## From code\n\n`runCli` takes the same arguments as the binary and returns `{ exitCode, stdout, stderr }`:\n\n```ts\nimport { assertResponse, runCli } from '@astryx-vue/cli'\n\nconst result = await runCli(['search', 'dialog', '--json'], { cwd: process.cwd() })\nconst found = assertResponse(result.stdout, 'search')\n```\n\n`assertResponse` narrows the envelope to the type you expect. `parseResponse` returns the envelope without asserting, and `isError` checks one.\n\nThis version does not include `swizzle`, `discover`, `gap-report`, `integration`, or `blog`, and there is no MCP server.",
|
|
76934
|
+
"zh": "# 命令行\n\n`@astryx-vue/cli` 是一个轻巧的命令行工具,用来在终端里随时查 astryx-vue 的组件文档和演练场页面,方便开发者和 AI Agent 使用。二进制命令名是 `astryx-vue`(请注意:它专为本项目服务,并非 Meta 原版的 `astryx` 命令,也不提供 React 或 StyleX 的功能)。\n\n建议在项目中作为开发依赖安装:\n\n```sh\npnpm add -D @astryx-vue/cli\n```\n\n包里声明了 `bin`,装完命令就在 `node_modules/.bin` 里,直接就能用:\n\n```sh\npnpm exec astryx-vue component Button\nnpx astryx-vue search dialog --json\n```\n\n不安装、只想临时用时,运行 `npx @astryx-vue/cli …`。注意 scope 不能省略,因为 npm 上裸名 `astryx-vue` 并不是这个包。\n\n脚本别名是可选项,只有当 agent 的环境没有把 `node_modules/.bin` 放进 `PATH` 时才需要在 `package.json` 里添加:\n\n```json\n{\n \"scripts\": {\n \"astryx-vue\": \"astryx-vue\"\n }\n}\n```\n\n配置后即可在终端运行:\n\n```sh\nnpm run astryx-vue -- component Button\n```\n\n## 核心命令\n\n- `astryx-vue component --list`:列出当前所有可用组件。\n- `astryx-vue component Button`:快速查看指定组件的基本用法、Props 属性、插槽、事件以及代码示例。\n- `astryx-vue composable --list`:列出组合式函数,`astryx-vue composable useClipboard` 打印其参数和返回值。Vue 里这类函数叫组合式函数(composable),所以命令与文档中统一不使用 hook 这个词。若误输入 `component useClipboard` 仍然能查到,并会友好提示改用 `composable` 命令。\n- `astryx-vue docs`:列出指南与主题文档。`astryx-vue docs guide/dark-mode` 或 `astryx-vue docs themes/tokens` 可直接在终端阅读正文。\n- `astryx-vue search dialog`:在组件、组合式函数、文档主题和页面模板里按相关度搜索。`--type` 可以限定为 `component`、`composable`、`doc` 或 `template`。\n- `astryx-vue manifest --json`:输出 CLI 自身的接口定义清单(命令、选项、响应结构、错误码及退出码),方便被上层工具或脚本消费。\n- `astryx-vue theme`:列出全部内置主题;`astryx-vue theme butter` 输出这套主题的接入四步(引哪个样式表、`data-astryx-theme` 写什么、怎么换主题或同时用两套、改完怎么验证)。\n- `astryx-vue upgrade`:读项目里 `@astryx-vue/core` 的版本,和这个 CLI 对应版本比较,打印[升级指南](/guide/migration)里要做的每一件事,再在项目源码里指出旧名字所在文件和行号。不联网,不改文件。\n- `astryx-vue doctor`:检查依赖装没装、版本对不对、主题样式引没引、主题属性设没设,只报告不改文件;有失败时退出码是 1。\n- `astryx-vue init`:打印给 AI 助手的接入说明(装哪些包、主题样式引哪一行、语言怎么提供),并在项目根目录维护 agent 上下文文件。\n\n## 页面模板与代码生成\n\n- `astryx-vue template --list`:列出 57 个演练场页面模板,按分类分组。目前所有模板都是页面;`--type block` 会给出一句说明,不报错。每行还会说明这个页面要什么:可复制的模板只依赖 `vue` 和 `@astryx-vue/core`,落地后不用再装别的包。\n- `astryx-vue template login-card`:打印一个模板的说明、用到的组件、要装的包和完整源码。`--skeleton` 只打印布局骨架(组件标签加上 `gap`、`padding`、`columns` 这类布局属性,不含文字、事件处理和原生 HTML 包裹层)。\n- `astryx-vue template login-card ./src/pages`:将模板代码生成并写出到 `src/pages/LoginCard.vue`。如果文件已存在,需要加上 `--overwrite`(或 `-f`)才会覆盖,避免误冲掉已有代码。复制时会自动把演示图片替换为内联的灰色占位图,不依赖额外的静态图片资源。\n- `astryx-vue build \"<想法>\"`:把一句话需求变成开发起点,在本地匹配出最接近的页面模板、备选方案以及需要搭配使用的组件与函数。打分完全在本地完成,无网络开销也无需调用外部大模型。\n\n## 常用参数\n\n- `--json`:以 JSON 格式输出带类型的结构化信封数据,适合被工具或脚本解析。模板相关的类型包括 `template.list`、`template.show`、`template.skeleton`、`template.copy`;`build` 返回 `build.help` 和 `build.kit`。出错时信封里带有错误码(如 `ERR_UNKNOWN_TEMPLATE`、`ERR_FILE_EXISTS` 等)。\n- `--dense`:精简输出文本,降低阅读噪音;与 `--json` 配合使用时会被自动忽略,保持 JSON 结构完整。\n- `--zh`:当文档具备中文翻译时,优先输出简体中文内容。\n- `--cwd`:指定项目目录,`template` 和 `init` 只往这个目录下写文件,`upgrade` 和 `doctor` 只在这个目录里读。\n\n## 配合 AI Agent(生成上下文文件)\n\n项目里没有任何 agent 文件时,`init` 默认创建 `AGENTS.md`。不加 `--agent` 时会优先写项目里已有的配置文件,检测顺序为 `AGENTS.md`、`CLAUDE.md`、`.cursorrules`、`.hermes.md`、`HERMES.md`。`--agent claude` 写 `CLAUDE.md`,`hermes` 写 `.hermes.md`,`cursor` 和 `codex` 写 `AGENTS.md`,`all` 则写所有已存在的已知文件。通过 `--agent-docs-path <file>` 也可以指定输出到任意文件。\n\n命令行工具在维护这些文件时,只会在以下固定的起止标记之间更新内容:\n\n```html\n<!-- Maintained by astryx-vue, do not edit manually -->\n...\n<!-- astryx-vue block end -->\n```\n\n区块开头列出这个 CLI 覆盖的每个包,能发 canary 的标出来;npm 上还没有的包会直说「还没发布到 npm」,并且不给安装命令,免得有人照着敲一个装不到的包名。随后是三步工作流:用 `build` 挑页面,用 `template` 复制,写代码前用 `component`、`composable` 或 `docs` 查询用法。\n\n`init` 打印的安装命令跟着项目走:先看 `packageManager` 字段,再看锁文件;两样都没有时把四种写法都列出来。\n\n每次运行命令,工具只会用最新的组件库信息替换这两个标记之间的内容,标记之外由你自己撰写的项目规则会完好无损地保留下来。如果文件已存在但尚未包含这些标记,工具会安全地将其追加到文件末尾。\n\n> **维护者提醒**:CLI 打印的内容均提取自当前文档站和演练场页面。当文档或模板修改后,需要重新生成 CLI 包内置的目录快照,操作步骤记录在 `packages/cli/CONTRIBUTING.md` 中。\n\n## 在代码里调用\n\n`runCli` 收的参数和命令行一致,返回 `{ exitCode, stdout, stderr }`:\n\n```ts\nimport { assertResponse, runCli } from '@astryx-vue/cli'\n\nconst result = await runCli(['search', 'dialog', '--json'], { cwd: process.cwd() })\nconst found = assertResponse(result.stdout, 'search')\n```\n\n`assertResponse` 按你要的类型校验信封;`parseResponse` 只解析不断言,`isError` 判断是不是错误信封。\n>\n> 这一版暂未包含 `swizzle`、`discover`、`gap-report`、`integration`、`blog`,也没有 MCP 服务。"
|
|
74984
76935
|
},
|
|
74985
76936
|
"docsUrl": {
|
|
74986
76937
|
"en": "https://astryx-vue.pages.dev/en/guide/cli",
|
|
@@ -75667,145 +77618,6 @@ const catalog = {
|
|
|
75667
77618
|
"zh": "https://astryx-vue.pages.dev/guide/responsive"
|
|
75668
77619
|
}
|
|
75669
77620
|
},
|
|
75670
|
-
{
|
|
75671
|
-
"id": "guide/richtext",
|
|
75672
|
-
"kind": "guide",
|
|
75673
|
-
"title": {
|
|
75674
|
-
"en": "Rich text",
|
|
75675
|
-
"zh": "富文本"
|
|
75676
|
-
},
|
|
75677
|
-
"summary": {
|
|
75678
|
-
"en": "`@astryx-vue/richtext` is a rich text editor package for Vue 3. It builds on the Lexical framework so users can type headings, bold text, lists, code blocks, and tables right in the browser with immediate visual feedback.",
|
|
75679
|
-
"zh": "`@astryx-vue/richtext` 是一个给 Vue 3 用的富文本编辑器组件包。底层基于 Lexical 框架,让用户在网页里直接输入标题、加粗文字、列表、代码块和表格,所见即所得。"
|
|
75680
|
-
},
|
|
75681
|
-
"headings": {
|
|
75682
|
-
"en": [
|
|
75683
|
-
"What this is",
|
|
75684
|
-
"Install",
|
|
75685
|
-
"Your first content",
|
|
75686
|
-
"`<RichTextEditor>` props",
|
|
75687
|
-
"Props",
|
|
75688
|
-
"Slots",
|
|
75689
|
-
"Events",
|
|
75690
|
-
"Methods on the template ref",
|
|
75691
|
-
"Read-only and disabled",
|
|
75692
|
-
"Toolbar",
|
|
75693
|
-
"Actions and keyboard shortcuts",
|
|
75694
|
-
"`<RichTextEditorToolbar>` props and slots",
|
|
75695
|
-
"Custom icons",
|
|
75696
|
-
"Plugins",
|
|
75697
|
-
"Default plugins",
|
|
75698
|
-
"Auto-link plugin: `RichTextEditorAutoLinkPlugin`",
|
|
75699
|
-
"Markdown in and out",
|
|
75700
|
-
"Pure conversion functions",
|
|
75701
|
-
"Source preservation on round trips",
|
|
75702
|
-
"Markdown plugin nodes",
|
|
75703
|
-
"Read-only view: RichTextView",
|
|
75704
|
-
"Design role and accessibility",
|
|
75705
|
-
"Keyboard",
|
|
75706
|
-
"Accessibility",
|
|
75707
|
-
"Not supported yet"
|
|
75708
|
-
],
|
|
75709
|
-
"zh": [
|
|
75710
|
-
"这是什么",
|
|
75711
|
-
"安装",
|
|
75712
|
-
"第一段内容",
|
|
75713
|
-
"`<RichTextEditor>` 的属性",
|
|
75714
|
-
"属性",
|
|
75715
|
-
"插槽",
|
|
75716
|
-
"事件",
|
|
75717
|
-
"模板引用暴露的方法",
|
|
75718
|
-
"只读与禁用",
|
|
75719
|
-
"工具栏",
|
|
75720
|
-
"工具栏功能与快捷键",
|
|
75721
|
-
"`<RichTextEditorToolbar>` 的属性与插槽",
|
|
75722
|
-
"自定义图标",
|
|
75723
|
-
"插件",
|
|
75724
|
-
"默认已开启的功能插件",
|
|
75725
|
-
"自动链接插件:`RichTextEditorAutoLinkPlugin`",
|
|
75726
|
-
"Markdown 进出",
|
|
75727
|
-
"纯函数转换",
|
|
75728
|
-
"原样往返设计",
|
|
75729
|
-
"Markdown 插件节点",
|
|
75730
|
-
"只读视图 RichTextView",
|
|
75731
|
-
"设计定位与无障碍考虑",
|
|
75732
|
-
"键盘操作",
|
|
75733
|
-
"无障碍",
|
|
75734
|
-
"现在不支持的"
|
|
75735
|
-
]
|
|
75736
|
-
},
|
|
75737
|
-
"keywords": {
|
|
75738
|
-
"en": [
|
|
75739
|
-
"label",
|
|
75740
|
-
"isLabelHidden",
|
|
75741
|
-
"description",
|
|
75742
|
-
"isOptional",
|
|
75743
|
-
"isRequired",
|
|
75744
|
-
"defaultValue",
|
|
75745
|
-
"placeholder",
|
|
75746
|
-
"isReadOnly",
|
|
75747
|
-
"isDisabled",
|
|
75748
|
-
"status",
|
|
75749
|
-
"statusVariant",
|
|
75750
|
-
"width",
|
|
75751
|
-
"minHeight",
|
|
75752
|
-
"labelTooltip",
|
|
75753
|
-
"size",
|
|
75754
|
-
"nodes",
|
|
75755
|
-
"extensions",
|
|
75756
|
-
"markdownExtensions",
|
|
75757
|
-
"hasMarkdownShortcuts",
|
|
75758
|
-
"transformers",
|
|
75759
|
-
"hasAutoFocus",
|
|
75760
|
-
"tabEscapeHint",
|
|
75761
|
-
"maxLength",
|
|
75762
|
-
"namespace",
|
|
75763
|
-
"headingLevels",
|
|
75764
|
-
"hasLink",
|
|
75765
|
-
"hasNewTabLinks",
|
|
75766
|
-
"promptForUrl"
|
|
75767
|
-
],
|
|
75768
|
-
"zh": [
|
|
75769
|
-
"属性名",
|
|
75770
|
-
"label",
|
|
75771
|
-
"isLabelHidden",
|
|
75772
|
-
"description",
|
|
75773
|
-
"isOptional",
|
|
75774
|
-
"isRequired",
|
|
75775
|
-
"defaultValue",
|
|
75776
|
-
"placeholder",
|
|
75777
|
-
"isReadOnly",
|
|
75778
|
-
"isDisabled",
|
|
75779
|
-
"status",
|
|
75780
|
-
"statusVariant",
|
|
75781
|
-
"width",
|
|
75782
|
-
"minHeight",
|
|
75783
|
-
"labelTooltip",
|
|
75784
|
-
"size",
|
|
75785
|
-
"nodes",
|
|
75786
|
-
"extensions",
|
|
75787
|
-
"markdownExtensions",
|
|
75788
|
-
"hasMarkdownShortcuts",
|
|
75789
|
-
"transformers",
|
|
75790
|
-
"hasAutoFocus",
|
|
75791
|
-
"tabEscapeHint",
|
|
75792
|
-
"maxLength",
|
|
75793
|
-
"namespace",
|
|
75794
|
-
"headingLevels",
|
|
75795
|
-
"hasLink",
|
|
75796
|
-
"hasNewTabLinks",
|
|
75797
|
-
"promptForUrl"
|
|
75798
|
-
]
|
|
75799
|
-
},
|
|
75800
|
-
"body": {
|
|
75801
|
-
"en": "# Rich text\n\n## What this is\n\n`@astryx-vue/richtext` is a rich text editor package for Vue 3. It builds on the Lexical framework so users can type headings, bold text, lists, code blocks, and tables right in the browser with immediate visual feedback.\n\nIt does three main things:\n1. Provides a ready-to-use `<RichTextEditor>` input area and an optional `<RichTextEditorToolbar>`.\n2. Turns `# ` or `- ` into headings and lists as you type (Markdown shortcuts), and exports the document back to Markdown without touching unchanged blocks.\n3. Provides `<RichTextView>`, a lightweight read-only component that renders saved content without the overhead of an editor.\n\n## Install\n\nThe editor depends on `@astryx-vue/core`, `vue`, and Meta's `lexical` packages. Run this command in your project terminal (keep all `lexical` and `@lexical/*` packages pinned to `0.52.0`):\n\n```bash\npnpm add @astryx-vue/richtext \\\n lexical@0.52.0 \\\n @lexical/code@0.52.0 \\\n @lexical/extension@0.52.0 \\\n @lexical/headless@0.52.0 \\\n @lexical/history@0.52.0 \\\n @lexical/html@0.52.0 \\\n @lexical/link@0.52.0 \\\n @lexical/list@0.52.0 \\\n @lexical/markdown@0.52.0 \\\n @lexical/rich-text@0.52.0 \\\n @lexical/selection@0.52.0 \\\n @lexical/table@0.52.0 \\\n @lexical/utils@0.52.0\n```\n\nImport the base styles in your application entry file (such as `main.ts`):\n\n```ts\nimport '@astryx-vue/richtext/richtext.css'\n```\n\n## Your first content\n\n`<RichTextEditor>` requires only the `label` prop to get started. Screen readers use this label to announce the field, so it is required for accessibility.\n\nHere is the shortest working example:\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Release notes\" placeholder=\"Type something here...\" />\n</template>\n```\n\nHere is the rendered result:\n\n## `<RichTextEditor>` props\n\nThis section covers all props, slots, events, and template ref methods supported by `<RichTextEditor>`.\n\n### Props\n\n| Name | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `label` | `string` | Required | Accessible label text for the editor. Screen readers require it. |\n| `isLabelHidden` | `boolean` | `false` | Visually hides the label while keeping it accessible to screen readers. |\n| `description` | `string` | - | Helper text displayed between the label and the editing area. |\n| `isOptional` | `boolean` | `false` | Marks the field as optional. Mutually exclusive with `isRequired`. |\n| `isRequired` | `boolean` | `false` | Marks the field as required. Mutually exclusive with `isOptional`. |\n| `defaultValue` | `string` | - | Initial content string produced by `JSON.stringify(editorState.toJSON())`. The editor is uncontrolled and reads this value only once upon mount. |\n| `placeholder` | `string` | - | Dim helper text displayed when the editing area is completely empty. |\n| `isReadOnly` | `boolean` | `false` | Read-only state. Text remains sharp and stays in the Tab order so users can select, copy, and read content without modifying it. |\n| `isDisabled` | `boolean` | `false` | Disabled state. Muted colors, non-editable, and removed from the keyboard Tab order. Takes precedence if both `isReadOnly` and `isDisabled` are true. |\n| `status` | `RichTextEditorStatus` | - | Validation status object `{ type: 'warning' \\| 'error' \\| 'success', message?: string }`. Sets the border color and renders an alert message below. |\n| `statusVariant` | `'attached' \\| 'detached' \\| 'tooltip'` | `'attached'` | Placement of the status alert message. |\n| `width` | `number \\| string` | - | Total width of the form field. Numbers are treated as pixels (px); strings are passed as CSS lengths. |\n| `minHeight` | `number \\| string` | `'4.5rem'` | Minimum height of the editable surface. Numbers are treated as pixels; strings are passed as CSS lengths. |\n| `labelTooltip` | `string` | - | Tooltip text shown when hovering the info icon next to the label. |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` | Size variant. Controls the inner padding of the editing box. |\n| `nodes` | `ReadonlyArray<Klass<LexicalNode>>` | - | Extra Lexical node classes to register beyond the defaults (headings, quote, lists, link, code block, table). |\n| `extensions` | `ReadonlyArray<AnyLexicalExtensionArgument>` | - | Extra Lexical extension arguments. |\n| `markdownExtensions` | `ReadonlyArray<RichTextMarkdownExtension>` | - | Markdown extension plugins created by `createRichTextExtension`. |\n| `hasMarkdownShortcuts` | `boolean` | `true` | Enables Markdown shortcuts such as typing `# ` for headings or `- ` for bullet lists. |\n| `transformers` | `ReadonlyArray<Transformer>` | Default transformers | Custom Markdown transformer rules. Defaults to standard transformers plus GFM tables. |\n| `hasAutoFocus` | `boolean` | `false` | Moves focus into the editing surface as soon as the component mounts. |\n| `tabEscapeHint` | `string` | Default copy | Instruction announced to screen readers on how to exit the editor. Because Tab indents text inside the editor, exiting requires pressing Escape then Tab. Pass an empty string to silence this hint. |\n| `maxLength` | `number` | - | Plain-text character limit. Displays a counter in the bottom-right corner. Text is not truncated when exceeding the limit, but the counter turns red. |\n| `namespace` | `string` | `'astryx-editor'` | Namespace for the underlying Lexical instance. Distinguishes multiple editors on the same page. |\n\n### Slots\n\n- `#toolbar`: Full-width slot positioned directly above the editing surface. Typically holds `<RichTextEditorToolbar />`.\n- `#default`: Slot inside the editing surface. Used for headless plugin components (such as an auto-link plugin or custom tools that call `useLexicalEditor()`).\n\n### Events\n\n- `@change`: Fires when the user edits the document. Receives two arguments:\n - `editorState: EditorState`: Current state object. Call `editorState.toJSON()` to serialize it for storage.\n - `editor: LexicalEditor`: Underlying Lexical editor instance.\n\n### Methods on the template ref\n\nBind a `ref` to `<RichTextEditor>` to call these instance methods:\n\n```ts\nconst editorRef = ref<RichTextEditorExposed>()\n\n// Focus the editing surface\neditorRef.value?.focus()\n\n// Clear all content and reset to a single empty paragraph\neditorRef.value?.clear()\n\n// Read the current EditorState object\nconst state = editorRef.value?.getEditorState()\n\n// Export current content as a Markdown string (preserves untouched blocks)\nconst markdown = editorRef.value?.getMarkdown()\n\n// Export current content as an HTML string (requires a browser DOM environment)\nconst html = editorRef.value?.getHTML()\n\n// Access the underlying LexicalEditor instance for advanced customization\nconst rawEditor = editorRef.value?.getEditor()\n```\n\nThis example shows how to listen to `@change` and call `clear()` and `getMarkdown()`:\n\n## Read-only and disabled\n\nThe editor provides two non-editable states: `isReadOnly` and `isDisabled`. They serve distinct interactive and accessibility needs:\n\n1. **Read-only (`isReadOnly`)**:\n - Text color remains at full contrast.\n - Remains in the keyboard Tab order. Screen readers announce the field as read-only.\n - Users can select and copy text.\n2. **Disabled (`isDisabled`)**:\n - The entire input and its text appear dimmed.\n - Completely removed from the Tab order; Tab skips past it.\n - Screen readers announce the field as disabled.\n - If both `isReadOnly` and `isDisabled` are passed to the same component, **disabled wins**.\n\n```vue\n<!-- Read-only: easy to inspect and copy, stays focusable and accessible -->\n<RichTextEditor label=\"Release notes\" :default-value=\"savedContent\" is-read-only />\n\n<!-- Disabled: dimmed and removed from the Tab order -->\n<RichTextEditor label=\"Release notes\" :default-value=\"savedContent\" is-disabled />\n```\n\nNotice the distinction between a read-only form field and a display view:\n- **`<RichTextEditor is-read-only>`**: Built for **forms**. It keeps its field label, helper description, and border frame. Semantically it remains a form control.\n- **`<RichTextView>`**: Built for **article bodies** (such as blog posts or detail views). It strips all form semantics, borders, and labels, rendering pure document content.\n\nHere is how the `status` prop and `maxLength` look in practice:\n\n## Toolbar\n\n`<RichTextEditorToolbar>` is the companion format bar for the editor. Place it in the `#toolbar` slot of `<RichTextEditor>` to dock it flush to the top of the editing area.\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorToolbar } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Weekly report\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n </RichTextEditor>\n</template>\n```\n\n### Actions and keyboard shortcuts\n\nThe toolbar includes common typographic controls:\n- **History**: Undo and Redo.\n- **Block formats**: Paragraph, Heading 1 (H1), Heading 2 (H2), Heading 3 (H3), Quote, Bullet List, and Numbered List.\n- **Inline styles**: Bold, Italic, Underline, Strikethrough, and Inline Code.\n- **Insert and edit links**: Click the link icon on the toolbar, or select text and press <kbd>Cmd</kbd> + <kbd>K</kbd> (<kbd>Ctrl</kbd> + <kbd>K</kbd> on Windows) to open the built-in popover.\n - Type a destination URL in the popover and press Enter to save. The text selection stays intact while the popover is open.\n - Every URL is sanitized through the built-in `sanitizeUrl` function. Only `http:`, `https:`, `mailto:`, and `tel:` protocols are allowed; scripts like `javascript:` are blocked.\n\n### `<RichTextEditorToolbar>` props and slots\n\n| Name | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `label` | `string` | Localized string | Accessible name for the entire toolbar (defaults to \"Text formatting\"). |\n| `headingLevels` | `ReadonlyArray<'h1' \\| 'h2' \\| 'h3'>` | `['h1', 'h2', 'h3']` | Which heading levels to display in the block format dropdown, in the given order. |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'sm'` | Size variant. Scales all buttons and dropdown menus inside the toolbar. |\n| `hasLink` | `boolean` | `true` | Shows the link button and enables the <kbd>Cmd/Ctrl</kbd> + <kbd>K</kbd> shortcut. |\n| `hasNewTabLinks` | `boolean` | `true` | Opens inserted links in a new tab by default (attaches `target=\"_blank\"` and `rel=\"noopener noreferrer\"`). |\n| `promptForUrl` | `(selectedText: string) => string \\| null \\| undefined` | - | Custom URL prompt callback. Use this to provide your own dialog instead of the built-in popover. |\n\nSlots:\n- `#end`: Custom slot rendered after all default buttons, separated by a vertical divider. Useful for project-specific buttons such as @mentions, AI helpers, or an emoji picker.\n\n### Custom icons\n\nThe toolbar ships with built-in SVG icons. To use icons from your own icon library (such as Lucide or Carbon), override them through the global theme helper `defineTheme`. Every icon key uses the `richtext:` prefix:\n\n```ts\nimport { defineTheme } from '@astryx-vue/core'\n\ndefineTheme({\n icons: {\n 'richtext:bold': 'lucide:bold',\n 'richtext:italic': 'lucide:italic',\n 'richtext:link': 'lucide:link',\n // See the list below for all replaceable keys\n },\n})\n```\n\n`RICHTEXT_ICON_KEYS` exports all replaceable icon keys:\n- `bold` -> `'richtext:bold'`\n- `italic` -> `'richtext:italic'`\n- `underline` -> `'richtext:underline'`\n- `strikethrough` -> `'richtext:strikethrough'`\n- `code` -> `'richtext:code'`\n- `link` -> `'richtext:link'`\n- `paragraph` -> `'richtext:paragraph'`\n- `h1`, `h2`, `h3` -> `'richtext:h1'`, `'richtext:h2'`, `'richtext:h3'`\n- `quote` -> `'richtext:quote'`\n- `bullet` -> `'richtext:bullet'`\n- `number` -> `'richtext:number'`\n- `undo` -> `'richtext:undo'`\n- `redo` -> `'richtext:redo'`\n\n## Plugins\n\nPlugins extend the editor with new behaviors, such as task checkboxes, code syntax highlighting, or turning typed URLs into links automatically.\n\n### Default plugins\n\nWhen you mount `<RichTextEditor>`, it includes these capabilities out of the box with no extra configuration:\n- **Task checkboxes**: Supports `- [ ]` and `- [x]` checklist syntax. Checkboxes can be toggled by clicking them and lock automatically when the editor is read-only.\n- **Code block header and copy button (CodeBlockHeaderPlugin)**: Displays the language name (such as `TypeScript` or `Python`) above code blocks, along with a one-click copy button.\n- **Code syntax highlighting (CodeSyntaxPlugin)**: Colors keywords, strings, numbers, and comments inside code blocks using the same theme variables as `CodeBlock`.\n- **Table width sizing and scrolling (Table Plugins)**: Enforces minimum column widths and adds accessible horizontal scrolling when tables overflow their container.\n\n### Auto-link plugin: `RichTextEditorAutoLinkPlugin`\n\nTurning typed or pasted URLs into clickable links is a frequent requirement. To keep the core package light, this feature is a headless Vue component plugin. Place it directly inside the default slot of `<RichTextEditor>`:\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorAutoLinkPlugin, RichTextEditorToolbar } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"Notes\" placeholder=\"Try pasting a link here...\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n <!-- Add the auto-link plugin -->\n <RichTextEditorAutoLinkPlugin />\n </RichTextEditor>\n</template>\n```\n\nThe plugin provides two default matchers (`DEFAULT_LINK_MATCHERS`): one for web URLs and one for email addresses. URLs without a protocol prefix receive `https://`, emails receive `mailto:`, and all generated links attach `target=\"_blank\"` and `rel=\"noopener noreferrer\"` to open safely in a new tab.\n\nTo recognize custom patterns (such as turning `#123` into an issue tracker link), pass custom rules to the `matchers` prop:\n\n```vue\n<RichTextEditorAutoLinkPlugin :matchers=\"myCustomMatchers\" @change=\"(url, prevUrl) => onLinkChange(url, prevUrl)\" />\n```\n\nThe plugin emits a `change` event whenever a link is added, modified, or removed, passing the new URL (`null` on removal) and the previous URL. Build `myCustomMatchers` with `createLinkMatcherWithRegExp` from `@lexical/link`.\n\n## Markdown in and out\n\nMost workflows edit rich text in the browser but store plain Markdown on the server. `@astryx-vue/richtext` provides a lossless round-trip conversion pipeline.\n\n### Pure conversion functions\n\nImport two conversion functions from `@astryx-vue/richtext/markdown`. This entry point has no browser DOM dependency and runs safely in server-side environments such as Node.js or Cloudflare Workers:\n\n```ts\nimport { editorStateJSONToMarkdown, markdownToEditorStateJSON } from '@astryx-vue/richtext/markdown'\n\n// 1. On the server or during initialization: convert Markdown text to JSON for defaultValue\nconst defaultValue = markdownToEditorStateJSON('# Welcome\\n\\nThis is **Markdown** text.')\n\n// 2. When saving or submitting: restore the JSON string back to Markdown text\nconst markdown = editorStateJSONToMarkdown(savedJson)\n```\n\n### Source preservation on round trips\n\nStandard rich text converters often scramble formatting during a Markdown -> editor -> Markdown cycle, dropping indentations or turning every `*` into a `-`.\n\nAstryx Rich Text uses a source-preservation mechanism: imported Markdown blocks keep their original punctuation, spacing, and indents intact on export as long as the user did not edit that block. Only modified or newly added blocks are reformatted with standard syntax.\n\n## Markdown plugin nodes\n\nProjects sometimes need custom Markdown syntax, such as `@username` mentions or custom alert callout blocks.\n\nUse `createRichTextExtension` to register a custom Markdown plugin:\n\n```ts\nimport { createRichTextExtension } from '@astryx-vue/richtext'\nimport { mentionPlugin } from './myMentionPlugin'\n\n// Wrap into an extension recognized by the editor\nconst mentionsExtension = createRichTextExtension(mentionPlugin)\n```\n\nPass the extension to `<RichTextEditor>` or `<RichTextView>` via the `markdownExtensions` prop:\n\n```vue\n<RichTextEditor\n label=\"Discussion\"\n :markdown-extensions=\"[mentionsExtension]\"\n/>\n```\n\nYou can also pass it to the conversion functions:\n\n```ts\nconst json = markdownToEditorStateJSON(markdownText, {\n extensions: [mentionsExtension],\n})\n```\n\n## Read-only view: RichTextView\n\nWhen you need to display an article, a blog post, or a release announcement without any editing capabilities, use the `<RichTextView>` component.\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextView } from '@astryx-vue/richtext'\n\n// The value is the JSON string saved earlier from editorState.toJSON()\nconst content = '{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Display content\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}'\n<\/script>\n\n<template>\n <RichTextView :value=\"content\" />\n</template>\n```\n\n### Design role and accessibility\n\n- **Document content, not a form control**: Even when set to non-editable, Lexical defaults to `role=\"textbox\"` on its root element. Tagging a multi-page article as a text input causes screen readers to misidentify it and triggers accessibility audit warnings. `<RichTextView>` strips all form input semantics upon mount and renders as a clean document container.\n- **In-place updates**: When the parent updates the `value` prop, `<RichTextView>` re-parses the content in place without tearing down and recreating the DOM tree.\n- **Resilient error handling**: If saved JSON cannot be parsed due to a version migration or corrupted data, the component will not crash the entire page. It emits a `@parseError` event and supports a fallback display through the `#errorFallback` slot:\n\n```vue\n<RichTextView :value=\"maybeCorruptedContent\" @parse-error=\"onParseError\">\n <template #errorFallback>\n <div class=\"error-box\">Unable to display document content.</div>\n </template>\n</RichTextView>\n```\n\n## Keyboard\n\nThe editor is optimized for full keyboard workflows:\n\n- **Tab indentation and escape**:\n - Pressing <kbd>Tab</kbd> in a paragraph or list item increases its indentation level; pressing <kbd>Shift</kbd> + <kbd>Tab</kbd> decreases it.\n - Because <kbd>Tab</kbd> is used for typography, keyboard-only and screen reader users need a reliable way to exit the editor. Following accessibility standards, **press <kbd>Escape</kbd> once, then press <kbd>Tab</kbd>** to move focus to the next interactive element on the page.\n - The editor announces this escape sequence to screen readers by default. You can change this copy with the `tabEscapeHint` prop, or silence it by passing `tabEscapeHint=\"\"`.\n- **Navigating tables**:\n - While editing inside a table cell, pressing arrow keys (<kbd>↑</kbd>, <kbd>↓</kbd>, <kbd>←</kbd>, <kbd>→</kbd>) moves the cursor smoothly between adjacent cells.\n- **Keyboard shortcuts**:\n - <kbd>Cmd</kbd> + <kbd>K</kbd> (macOS) or <kbd>Ctrl</kbd> + <kbd>K</kbd> (Windows): Opens the link editing popover.\n - <kbd>Cmd/Ctrl</kbd> + <kbd>B</kbd>: Bold.\n - <kbd>Cmd/Ctrl</kbd> + <kbd>I</kbd>: Italic.\n - <kbd>Cmd/Ctrl</kbd> + <kbd>Z</kbd>: Undo; <kbd>Cmd/Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Z</kbd>: Redo.\n - Underline has no default shortcut; use the toolbar button.\n\n## Accessibility\n\nAstryx Rich Text includes extensive accessibility (a11y) considerations:\n\n1. **Explicit label association**: The required `label` prop creates an accessible name for the editor. Even when `isLabelHidden` is true, the label is preserved for screen readers via `aria-label`.\n2. **Read-only vs. disabled semantics**: `isReadOnly` applies `aria-readonly=\"true\"` and keeps `tabindex=\"0\"`, announcing the field as read-only text; `isDisabled` applies `aria-disabled=\"true\"` and removes the field from the Tab sequence entirely.\n3. **Character count feedback**: When `maxLength` is set, an off-screen `aria-live=\"polite\"` region alerts screen reader users when character count nears the threshold (over 80%) or exceeds it. Error states set `aria-invalid=\"true\"` automatically.\n4. **Preserved focus and selection**: Opening toolbar dialogs (such as the link popover) preserves the active text selection in the editor. Focus returns smoothly to the text area when the popover closes.\n\n## Not supported yet\n\nTo keep the Vue implementation clean and maintainable, this package intentionally differs from the upstream React implementation in two areas:\n\n1. **No `xstyle` styling system**: The upstream React package uses an internal `xstyle` object system, which is omitted in the Vue port. Use standard Vue `class` and `style` attributes to customize appearance.\n2. **Plugins use Vue component slots**: Upstream passes plugins as an array of React nodes; in `@astryx-vue/richtext`, plugins are headless, logic-only Vue components (such as `<RichTextEditorAutoLinkPlugin />`) placed directly in the default slot of `<RichTextEditor>`.",
|
|
75802
|
-
"zh": "# 富文本\n\n## 这是什么\n\n`@astryx-vue/richtext` 是一个给 Vue 3 用的富文本编辑器组件包。底层基于 Lexical 框架,让用户在网页里直接输入标题、加粗文字、列表、代码块和表格,所见即所得。\n\n它主要帮你做三件事:\n1. 提供开箱即用的富文本编辑框 `<RichTextEditor>` 与顶部格式工具栏 `<RichTextEditorToolbar>`。\n2. 支持输入 `# ` 或 `- ` 自动生成标题与列表(Markdown 快捷输入),并能原样把内容导出成 Markdown。\n3. 提供纯展示的只读视图 `<RichTextView>`,用轻量的方式把存下来的文档内容画出来。\n\n## 安装\n\n富文本编辑器依赖同级的 `@astryx-vue/core`、`vue` 以及 Meta 的 `lexical` 系列包。请在项目终端里执行安装命令(注意:所有 `@lexical/*` 和 `lexical` 的版本必须保持在 `0.52.0`):\n\n```bash\npnpm add @astryx-vue/richtext \\\n lexical@0.52.0 \\\n @lexical/code@0.52.0 \\\n @lexical/extension@0.52.0 \\\n @lexical/headless@0.52.0 \\\n @lexical/history@0.52.0 \\\n @lexical/html@0.52.0 \\\n @lexical/link@0.52.0 \\\n @lexical/list@0.52.0 \\\n @lexical/markdown@0.52.0 \\\n @lexical/rich-text@0.52.0 \\\n @lexical/selection@0.52.0 \\\n @lexical/table@0.52.0 \\\n @lexical/utils@0.52.0\n```\n\n接着在你的应用入口文件(如 `main.ts`)里引入编辑器的基础样式:\n\n```ts\nimport '@astryx-vue/richtext/richtext.css'\n```\n\n## 第一段内容\n\n使用 `<RichTextEditor>` 最少只要传一个 `label` 属性。`label` 是给读屏软件识别这个输入框用途的名字,为了无障碍体验,它是必填项。\n\n这是一段最短的可运行代码:\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"发布说明\" placeholder=\"请在这里输入内容...\" />\n</template>\n```\n\n实际运行效果如下:\n\n## `<RichTextEditor>` 的属性\n\n本节列出 `<RichTextEditor>` 支持的所有属性、插槽、事件以及模板引用暴露的方法。\n\n### 属性\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `label` | `string` | 必填 | 编辑器的标签文字。无障碍读屏软件需要用到它,因此必须填写。 |\n| `isLabelHidden` | `boolean` | `false` | 是否只对读屏软件朗读标签,视觉上把标签隐藏起来。 |\n| `description` | `string` | - | 摆在标签和编辑区之间的详细说明文字。 |\n| `isOptional` | `boolean` | `false` | 是否标记为选填。和 `isRequired` 互斥。 |\n| `isRequired` | `boolean` | `false` | 是否标记为必填。和 `isOptional` 互斥。 |\n| `defaultValue` | `string` | - | 初始内容字符串(由 `JSON.stringify(editorState.toJSON())` 得到)。编辑器是非受控的,只在创建挂载时读取一次。 |\n| `placeholder` | `string` | - | 编辑区完全为空时显示的提示灰字。 |\n| `isReadOnly` | `boolean` | `false` | 是否只读。文字保持原有清晰度,键盘 Tab 键依然能跳进来,方便复制或读屏,但不能改动文字。 |\n| `isDisabled` | `boolean` | `false` | 是否禁用。不可编辑、颜色变淡,且会从键盘 Tab 顺序中移除。若与 `isReadOnly` 同时为 true,以禁用为准。 |\n| `status` | `RichTextEditorStatus` | - | 校验状态对象 `{ type: 'warning' \\| 'error' \\| 'success', message?: string }`。决定边框颜色并在下方显示提示信息。 |\n| `statusVariant` | `'attached' \\| 'detached' \\| 'tooltip'` | `'attached'` | 状态提示信息的摆放位置。 |\n| `width` | `number \\| string` | - | 整个表单字段的宽度。传入数字当像素(px),传入字符串原样作为 CSS 长度。 |\n| `minHeight` | `number \\| string` | `'4.5rem'` | 编辑区域的最小高度。传入数字当像素,传入字符串作为 CSS 长度。 |\n| `labelTooltip` | `string` | - | 标签右侧信息小图标悬浮时出现的提示文本。 |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` | 编辑器尺寸,影响编辑框的内部留白(内边距)。 |\n| `nodes` | `ReadonlyArray<Klass<LexicalNode>>` | - | 在默认节点(标题、引用、列表、链接、代码块、表格)之外额外注册的 Lexical 节点类型。 |\n| `extensions` | `ReadonlyArray<AnyLexicalExtensionArgument>` | - | 额外的 Lexical 扩展插件数组。 |\n| `markdownExtensions` | `ReadonlyArray<RichTextMarkdownExtension>` | - | 要识别的 Markdown 插件扩展数组,由 `createRichTextExtension` 生成。 |\n| `hasMarkdownShortcuts` | `boolean` | `true` | 是否开启 Markdown 快捷输入(例如行首打 `# ` 变大标题,打 `- ` 变无序列表)。 |\n| `transformers` | `ReadonlyArray<Transformer>` | 默认转换器 | 自定义 Markdown 转换规则。如果不传,默认使用标准转换器加 GFM 表格。 |\n| `hasAutoFocus` | `boolean` | `false` | 组件挂载完成后是否自动把输入光标聚焦到编辑区。 |\n| `tabEscapeHint` | `string` | 默认文案 | 给读屏软件朗读的退出编辑提示。因为 Tab 键在编辑器里用来缩进文字,离开编辑器需要先按 Escape 再按 Tab。传空字符串表示不输出这段提示。 |\n| `maxLength` | `number` | - | 纯文字字数上限。设置后会在右下角显示计数;超出后不会截断文字,但计数器会变红提醒。 |\n| `namespace` | `string` | `'astryx-editor'` | 底层 Lexical 实例的命名空间,同一页面有多个编辑器时用来互相区分。 |\n\n### 插槽\n\n- `#toolbar`:放在编辑区正上方、顶到两边的工具栏插槽。通常在这里放 `<RichTextEditorToolbar />`。\n- `#default`:编辑区内部的插槽。可以放无界面的扩展组件(例如调用 `useLexicalEditor()` 的自动链接插件或自定义功能组件)。\n\n### 事件\n\n- `@change`:当用户修改了编辑内容时触发。提供两个参数:\n - `editorState: EditorState`:最新的编辑器状态对象,可以用 `editorState.toJSON()` 转成 JSON 存进数据库。\n - `editor: LexicalEditor`:底层的 Lexical 编辑器实例。\n\n### 模板引用暴露的方法\n\n给 `<RichTextEditor>` 绑定 `ref` 后,可以调用以下实例方法:\n\n```ts\nconst editorRef = ref<RichTextEditorExposed>()\n\n// 聚焦到编辑区\neditorRef.value?.focus()\n\n// 清空所有内容,恢复成一个空段落\neditorRef.value?.clear()\n\n// 读取当前的编辑器状态对象\nconst state = editorRef.value?.getEditorState()\n\n// 把当前内容导出为 Markdown 字符串(原本未修改的块保持原样)\nconst markdown = editorRef.value?.getMarkdown()\n\n// 把当前内容导出为 HTML 字符串(依赖浏览器 DOM 环境)\nconst html = editorRef.value?.getHTML()\n\n// 获取底层的 LexicalEditor 实例,供深度定制使用\nconst rawEditor = editorRef.value?.getEditor()\n```\n\n下面的例子展示了监听 `@change` 事件以及调用 `clear()` 和 `getMarkdown()` 的用法:\n\n## 只读与禁用\n\n编辑器提供了 `isReadOnly`(只读)和 `isDisabled`(禁用)两种不可编辑状态。它们在交互和无障碍上有明确的分工:\n\n1. **只读(`isReadOnly`)**:\n - 内容文字颜色不淡化,保持清晰可读。\n - 依然保留在键盘 Tab 键的切焦顺序中,视障用户通过键盘切过来时,读屏软件会明确朗读它是「只读」文本。\n - 用户可以选中里面的文字进行复制。\n2. **禁用(`isDisabled`)**:\n - 整个输入框与文字都会变淡变灰。\n - 彻底从键盘 Tab 键顺序中移出,按 Tab 键会直接跳过它。\n - 读屏软件会明确朗读它是「禁用」。\n - 如果同一个组件上同时写了 `isReadOnly` 和 `isDisabled`,**以禁用为准**。\n\n```vue\n<!-- 只读模式:方便查看和复制,且支持键盘聚焦与读屏 -->\n<RichTextEditor label=\"发布说明\" :default-value=\"savedContent\" is-read-only />\n\n<!-- 禁用模式:灰显且不在键盘切换顺序中 -->\n<RichTextEditor label=\"发布说明\" :default-value=\"savedContent\" is-disabled />\n```\n\n注意区分带标签的只读字段与纯展示视图:\n- **`<RichTextEditor is-read-only>`**:适用于**表单场景**。它带有字段标签(Label)、辅助说明与边框外框,语义上依然是一个表单输入框(只读控件)。\n- **`<RichTextView>`**:适用于**正文展示场景**(比如文章详情页、博客正文)。它没有外层表单框,剥离了所有表单语义,只是纯粹渲染文档内容。\n\n校验状态属性 `status` 与字数上限 `maxLength` 的展示效果如下:\n\n## 工具栏\n\n`<RichTextEditorToolbar>` 是配合编辑器使用的格式控制栏。把它放在 `<RichTextEditor>` 的 `#toolbar` 插槽里,就会贴合固定在编辑区顶部。\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorToolbar } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"工作周报\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n </RichTextEditor>\n</template>\n```\n\n### 工具栏功能与快捷键\n\n工具栏内置了常用的排版能力:\n- **历史记录**:撤销(Undo)与重做(Redo)。\n- **块级格式**:段落正文、一级标题(H1)、二级标题(H2)、三级标题(H3)、引用块(Quote)、无序圆点列表(Bullet List)、有序数字列表(Numbered List)。\n- **行内样式**:加粗(Bold)、斜体(Italic)、下划线(Underline)、删除线(Strikethrough)、行内代码(Code)。\n- **插入与编辑链接**:点击工具栏的链接图标,或者在选中文本后按下快捷键 <kbd>Cmd</kbd> + <kbd>K</kbd>(Windows 上为 <kbd>Ctrl</kbd> + <kbd>K</kbd>),会弹出内置的链接浮层。\n - 在弹出的浮层里输入地址并回车即可插入链接。浮层打开时选区依然保留,不会导致光标跑丢。\n - 所有输入的链接地址都会经过内置的 `sanitizeUrl` 校验清洗,系统只放行 `http:`、`https:`、`mailto:` 和 `tel:` 开头的安全协议,阻断 `javascript:` 等恶意脚本。\n\n### `<RichTextEditorToolbar>` 的属性与插槽\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `label` | `string` | 本地化文案 | 工具栏整体的无障碍名称(默认为「文字格式」)。 |\n| `headingLevels` | `ReadonlyArray<'h1' \\| 'h2' \\| 'h3'>` | `['h1', 'h2', 'h3']` | 在「块格式」下拉菜单里显示哪几级标题,按数组顺序排列。 |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'sm'` | 工具栏尺寸,同步缩放里面所有按钮和下拉选择框。 |\n| `hasLink` | `boolean` | `true` | 是否显示链接按钮,以及是否开启 <kbd>Cmd/Ctrl</kbd> + <kbd>K</kbd> 快捷键。 |\n| `hasNewTabLinks` | `boolean` | `true` | 插入的链接是否默认在新标签页打开(新标签页会自带 `target=\"_blank\"` 与 `rel=\"noopener noreferrer\"`)。 |\n| `promptForUrl` | `(selectedText: string) => string \\| null \\| undefined` | - | 自定义输入网址的回调。如果不想使用内置链接弹窗,可以传入此函数自行弹窗获取网址。 |\n\n插槽:\n- `#end`:尾部自定义插槽。内容会渲染在所有默认按钮右侧(中间以一条竖分割线隔开),方便放入你自己的业务按钮,例如 @提及人、AI 帮写或表情选择器。\n\n### 自定义图标\n\n工具栏内置了一套简洁的 SVG 图标。如果你想换成项目自己的图标库(例如 Lucide 或 Carbon),可以通过全局主题配置 `defineTheme` 替换,每个图标键都带有 `richtext:` 前缀:\n\n```ts\nimport { defineTheme } from '@astryx-vue/core'\n\ndefineTheme({\n icons: {\n 'richtext:bold': 'lucide:bold',\n 'richtext:italic': 'lucide:italic',\n 'richtext:link': 'lucide:link',\n // 支持替换的键见下表\n },\n})\n```\n\n`RICHTEXT_ICON_KEYS` 导出了所有支持替换的图标键:\n- `bold`(加粗) -> `'richtext:bold'`\n- `italic`(斜体) -> `'richtext:italic'`\n- `underline`(下划线) -> `'richtext:underline'`\n- `strikethrough`(删除线) -> `'richtext:strikethrough'`\n- `code`(行内代码) -> `'richtext:code'`\n- `link`(超链接) -> `'richtext:link'`\n- `paragraph`(正文段落) -> `'richtext:paragraph'`\n- `h1`、`h2`、`h3`(标题) -> `'richtext:h1'`、`'richtext:h2'`、`'richtext:h3'`\n- `quote`(引用) -> `'richtext:quote'`\n- `bullet`(无序列表) -> `'richtext:bullet'`\n- `number`(有序列表) -> `'richtext:number'`\n- `undo`(撤销) -> `'richtext:undo'`\n- `redo`(重做) -> `'richtext:redo'`\n\n## 插件\n\n插件能为编辑器扩展新玩法,例如检测任务勾选、代码块高亮或者网址自动变超链接。\n\n### 默认已开启的功能插件\n\n当你使用 `<RichTextEditor>` 时,编辑器内部已经默认集成了以下几项能力,不需要你额外配置:\n- **任务勾选框(Task Checkbox)**:支持 `- [ ]` 和 `- [x]` 任务清单语法,生成的勾选框可以直接点击勾选或取消,在只读模式下会自动锁定不可点。\n- **代码块标题栏与复制按钮(CodeBlockHeaderPlugin)**:遇到代码块时,顶部会展示语言名称(如 `TypeScript`、`Python`)并附带一键复制代码的按钮。\n- **代码语法高亮(CodeSyntaxPlugin)**:对代码块里的关键字、字符串、数字等按语法上色,颜色和 `CodeBlock` 用同一套语法色变量。\n- **表格列宽自适应与横向滚动(Table Plugins)**:表格列设定了最小宽度保护,并在超宽时提供无障碍横向滚动支持。\n\n### 自动链接插件:`RichTextEditorAutoLinkPlugin`\n\n输入或粘贴一段网址时,自动把它转成可点击的超链接是一项常见需求。为了保证轻量,这项能力做成了无界面的 Vue 组件插件。使用时直接放在 `<RichTextEditor>` 的默认插槽里:\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextEditor, RichTextEditorAutoLinkPlugin, RichTextEditorToolbar } from '@astryx-vue/richtext'\n<\/script>\n\n<template>\n <RichTextEditor label=\"便签\" placeholder=\"粘贴网址试试看...\">\n <template #toolbar>\n <RichTextEditorToolbar />\n </template>\n <!-- 放入自动链接插件 -->\n <RichTextEditorAutoLinkPlugin />\n </RichTextEditor>\n</template>\n```\n\n插件默认自带了网址和电子邮箱两个匹配器(`DEFAULT_LINK_MATCHERS`)。识别出的网址如果没写协议会自动补齐 `https://`,邮箱会自动加上 `mailto:`,并且生成的链接都会加上 `target=\"_blank\"` 和 `rel=\"noopener noreferrer\"` 以便安全地在新窗口打开。\n\n如果你想识别自定义语法(例如把 `#123` 自动变成工单链接),可以通过 `matchers` 属性传入自定义的匹配规则:\n\n```vue\n<RichTextEditorAutoLinkPlugin :matchers=\"myCustomMatchers\" @change=\"(url, prevUrl) => onLinkChange(url, prevUrl)\" />\n```\n\n自动加上、改掉或去掉链接时,插件会发出 `change` 事件,参数是新网址(去掉时是 `null`)和原来的网址。`myCustomMatchers` 用 `@lexical/link` 的 `createLinkMatcherWithRegExp` 来建。\n\n## Markdown 进出\n\n大部分时候,我们在前端编辑富文本,后端想存的却是通用的 Markdown 纯文本。`@astryx-vue/richtext` 提供了「无损往返」的转换机制。\n\n### 纯函数转换\n\n你可以从 `@astryx-vue/richtext/markdown` 导出两个转换函数。这个入口不依赖浏览器 DOM,在 Node.js、Cloudflare Workers 等服务端渲染(SSR)环境里都可以安全调用:\n\n```ts\nimport { editorStateJSONToMarkdown, markdownToEditorStateJSON } from '@astryx-vue/richtext/markdown'\n\n// 1. 服务端或初始化时:把 Markdown 字符串转成给 defaultValue 用的 JSON 字符串\nconst defaultValue = markdownToEditorStateJSON('# 欢迎\\n\\n这是一段 **Markdown** 文本。')\n\n// 2. 保存或提交时:把编辑器导出的 JSON 字符串还原回 Markdown 文本\nconst markdown = editorStateJSONToMarkdown(savedJson)\n```\n\n### 原样往返设计\n\n传统的富文本转换器在「Markdown -> 编辑器 -> 重新导出 Markdown」这个过程中,经常会打乱排版,比如把使用者的缩进弄丢、把 `*` 号全改成 `-` 号。\n\nAstryx 富文本采用了一套源码保留机制:对于导入进来的 Markdown 文本,只要用户在编辑时没有改动这个段落,导出时就会**原封不动保持用户最初写下的每一个标点和空格**;只有被改动过或全新写出的段落,才会使用规范的语法格式化输出。\n\n## Markdown 插件节点\n\n有些时候,项目里会有一些定制的 Markdown 语法,比如用 `@用户名` 提及同事、用特定指令插入告警框等。\n\n你可以使用 `createRichTextExtension` 函数把自定义的 Markdown 语法插件接入进来:\n\n```ts\nimport { createRichTextExtension } from '@astryx-vue/richtext'\nimport { mentionPlugin } from './myMentionPlugin'\n\n// 包装成编辑器能识别的扩展\nconst mentionsExtension = createRichTextExtension(mentionPlugin)\n```\n\n然后在 `<RichTextEditor>` 或 `<RichTextView>` 上通过 `markdownExtensions` 属性传给它们:\n\n```vue\n<RichTextEditor\n label=\"讨论区\"\n :markdown-extensions=\"[mentionsExtension]\"\n/>\n```\n\n在转换函数中也可以传入:\n\n```ts\nconst json = markdownToEditorStateJSON(markdownText, {\n extensions: [mentionsExtension],\n})\n```\n\n## 只读视图 RichTextView\n\n当你只需要在页面上展示文章、博客或发版公告,而不需要任何输入编辑功能时,应该使用 `<RichTextView>` 组件。\n\n```vue\n<script setup lang=\"ts\">\nimport { RichTextView } from '@astryx-vue/richtext'\n\n// 这里的 value 是先前通过 editorState.toJSON() 存下的 JSON 字符串\nconst content = '{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"这是展示内容\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}'\n<\/script>\n\n<template>\n <RichTextView :value=\"content\" />\n</template>\n```\n\n### 设计定位与无障碍考虑\n\n- **是文档内容,不是表单控件**:底层 Lexical 即使在不可编辑状态下,默认也会给根节点套上 `role=\"textbox\"`。但如果展示一整篇几千字的文章却被标记为「文本输入框」,视障用户的读屏软件就无法正确按文章阅读,无障碍检测工具也会报错。`<RichTextView>` 会在挂载时主动去除所有表单输入框语义,把它还原为一个纯粹的文档容器。\n- **就地热更新**:当父组件更新了 `value` 属性时,内部会就地切换解析状态,不会频繁销毁和重新挂载整个 DOM 结构。\n- **错误容灾机制**:从后端读出的历史 JSON 如果因为版本升级或格式损坏导致无法解析,组件不会让整个页面崩溃白屏。它会触发 `@parseError` 事件,并且可以通过 `#errorFallback` 插槽展示友好的降级提示界面:\n\n```vue\n<RichTextView :value=\"maybeCorruptedContent\" @parse-error=\"onParseError\">\n <template #errorFallback>\n <div class=\"error-box\">抱歉,文档内容加载失败。</div>\n </template>\n</RichTextView>\n```\n\n## 键盘操作\n\n编辑器针对键盘使用者进行了深度优化:\n\n- **Tab 缩进与退出**:\n - 在正文段落或列表里按下 <kbd>Tab</kbd> 键,会增加当前行的文字缩进;按下 <kbd>Shift</kbd> + <kbd>Tab</kbd> 会减少缩进。\n - 由于 <kbd>Tab</kbd> 键被用来做文本排版,视障或全键盘用户需要能够把焦点移出编辑器。编辑器遵循无障碍规范:**先按一次 <kbd>Escape</kbd> 键,再按 <kbd>Tab</kbd> 键**,焦点就会跳出编辑器,移动到网页的下一个可交互元素上。\n - 编辑器默认会向读屏软件朗读这段退出指引。如果你想自定义朗读文本,可以修改 `tabEscapeHint` 属性;传空字符串 `tabEscapeHint=\"\"` 则可以关闭该朗读。\n- **表格中的键盘移动**:\n - 在表格单元格中输入时,按下方向键(<kbd>↑</kbd>、<kbd>↓</kbd>、<kbd>←</kbd>、<kbd>→</kbd>)可以在相邻单元格之间平滑穿梭移动。\n- **快捷键**:\n - <kbd>Cmd</kbd> + <kbd>K</kbd>(macOS)或 <kbd>Ctrl</kbd> + <kbd>K</kbd>(Windows):快速打开超链接设置弹窗。\n - <kbd>Cmd/Ctrl</kbd> + <kbd>B</kbd>:粗体。\n - <kbd>Cmd/Ctrl</kbd> + <kbd>I</kbd>:斜体。\n - <kbd>Cmd/Ctrl</kbd> + <kbd>Z</kbd>:撤销;<kbd>Cmd/Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Z</kbd>:重做。\n - 下划线没有键盘快捷键,用工具栏上的按钮。\n\n## 无障碍\n\nAstryx 富文本编辑器在无障碍体验(a11y)上做了大量细节处理:\n\n1. **清晰的 Label 关联**:通过必填的 `label` 属性,外层自动建立无障碍名称映射。即便设置了 `isLabelHidden`,该名称依然会通过 `aria-label` 忠实传达给读屏软件。\n2. **区分只读与禁用**:`isReadOnly` 会挂载 `aria-readonly=\"true\"` 并保持 `tabindex=\"0\"`,读屏朗读为只读文本;`isDisabled` 会挂载 `aria-disabled=\"true\"` 并彻底移除 Tab 聚焦顺序。\n3. **字数与表单状态联动**:当配置了 `maxLength` 时,实时的纯文本字数会在接近上限(超过 80%)及超额时,通过 `aria-live=\"polite\"` 的隐藏广播区向读屏用户报读剩余或超出的字数;错误状态时会自动设置 `aria-invalid=\"true\"`。\n4. **焦点与选区保护**:工具栏中的弹窗(如添加链接)在开启时,不会销毁编辑区原有的文本选区;关闭弹窗后焦点能够平滑归还。\n\n## 现在不支持的\n\n为了保持 Vue 实现的纯粹性与稳定性,本包与上游 React 原版实现存在以下差异:\n\n1. **不支持 `xstyle` 样式方案**:上游 React 版本依赖内部的 `xstyle` 样式对象机制,在 Vue 移植版中不予支持。请统一使用标准的 Vue `class` 和 `style` 属性来定义外观。\n2. **插件机制为 Vue 组件插槽**:上游的 `plugins` 是作为 React 节点数组传递的;而在 `@astryx-vue/richtext` 中,扩展插件是纯逻辑的无界面 Vue 组件(例如 `<RichTextEditorAutoLinkPlugin />`),直接写在 `<RichTextEditor>` 的默认插槽内部即可。"
|
|
75803
|
-
},
|
|
75804
|
-
"docsUrl": {
|
|
75805
|
-
"en": "https://astryx-vue.pages.dev/en/guide/richtext",
|
|
75806
|
-
"zh": "https://astryx-vue.pages.dev/guide/richtext"
|
|
75807
|
-
}
|
|
75808
|
-
},
|
|
75809
77621
|
{
|
|
75810
77622
|
"id": "guide/styling",
|
|
75811
77623
|
"kind": "guide",
|
|
@@ -77835,7 +79647,7 @@ const catalog = {
|
|
|
77835
79647
|
],
|
|
77836
79648
|
"dependencies": ["@astryx-vue/core", "vue"],
|
|
77837
79649
|
"isReady": true,
|
|
77838
|
-
"source": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue'\nimport {\n Badge,\n Banner,\n Button,\n Card,\n Center,\n CheckboxInput,\n Collapsible,\n Divider,\n Grid,\n HStack,\n Icon,\n Layout,\n LayoutContent,\n Link,\n NumberInput,\n RadioList,\n RadioListItem,\n Section,\n Selector,\n Stack,\n StackItem,\n Text,\n TextArea,\n TextInput,\n Thumbnail,\n useMediaQuery,\n VStack,\n} from '@astryx-vue/core'\nimport { computed, ref } from 'vue'\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\nconst MONTHS = [\n '01',\n '02',\n '03',\n '04',\n '05',\n '06',\n '07',\n '08',\n '09',\n '10',\n '11',\n '12',\n]\nconst YEARS = Array.from({ length: 12 }, (_, i) => String(2025 + i))\n\nconst US_STATES = [\n 'Alabama',\n 'Alaska',\n 'Arizona',\n 'Arkansas',\n 'California',\n 'Colorado',\n 'Connecticut',\n 'Delaware',\n 'Florida',\n 'Georgia',\n 'Hawaii',\n 'Idaho',\n 'Illinois',\n 'Indiana',\n 'Iowa',\n 'Kansas',\n 'Kentucky',\n 'Louisiana',\n 'Maine',\n 'Maryland',\n 'Massachusetts',\n 'Michigan',\n 'Minnesota',\n 'Mississippi',\n 'Missouri',\n 'Montana',\n 'Nebraska',\n 'Nevada',\n 'New Hampshire',\n 'New Jersey',\n 'New Mexico',\n 'New York',\n 'North Carolina',\n 'North Dakota',\n 'Ohio',\n 'Oklahoma',\n 'Oregon',\n 'Pennsylvania',\n 'Rhode Island',\n 'South Carolina',\n 'South Dakota',\n 'Tennessee',\n 'Texas',\n 'Utah',\n 'Vermont',\n 'Virginia',\n 'Washington',\n 'West Virginia',\n 'Wisconsin',\n 'Wyoming',\n]\n\n// Product photos from the local template-assets set (committed to the\n// docsite; the CLI swaps these for an inline placeholder on scaffold).\nconst ITEM_IMAGES: Record<string, { src: string }> = {\n 1: {\n src: '/template-assets/light-product-1.png',\n },\n 2: {\n src: '/template-assets/light-product-4.png',\n },\n 3: {\n src: '/template-assets/light-product-5.png',\n },\n}\n\nconst ORDER_ITEMS = [\n {\n id: '1',\n name: 'Speckled Stoneware Mug',\n variant: 'Hand-thrown · 12 oz',\n price: 78,\n qty: 1,\n limited: false,\n },\n {\n id: '2',\n name: 'Stoneware Dinner Plate',\n variant: 'Reactive glaze · 10 in',\n price: 72,\n qty: 1,\n limited: false,\n },\n {\n id: '3',\n name: 'Cereal Bowl',\n variant: 'Speckled clay · 6 in',\n price: 80,\n qty: 1,\n limited: true,\n },\n]\n\nconst SUBTOTAL = 230\n// SHIPPING is now computed from deliveryMethod state\nconst TAX = 18.4\n// TOTAL is computed dynamically based on delivery selection\nconst fmt = (n: number) => `$${n.toFixed(2)}`\n\nconst CARD_LOGOS = [\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/visa.svg', alt: 'Visa' },\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/mastercard.svg', alt: 'Mastercard' },\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/amex.svg', alt: 'Amex' },\n]\n\nconst POLICIES = ['Refund policy', 'Privacy policy', 'Terms of service', 'Cancellations']\n\n// ── Styles ────────────────────────────────────────────────────────────────────\n// Plain inline styles using Astryx design-token CSS variables (declared at\n// :root by `@astryxdesign/core/astryx.css`). No StyleX compiler required.\n\nconst fullWidth: CSSProperties = { width: '100%' }\n// Form column flex-basis so the two checkout columns share width evenly.\nconst formColBasis: CSSProperties = { flexBasis: 0 }\n// Space the Order Summary content below its collapsible trigger title.\nconst summaryContent: CSSProperties = { paddingBlockStart: 'var(--astryx-spacing-2)' }\n// Order-summary column: sticky beside the form on desktop.\nconst summarySticky: CSSProperties = {\n flexBasis: 0,\n position: 'sticky',\n top: 'var(--astryx-spacing-4)',\n alignSelf: 'flex-start',\n}\n// On mobile the summary moves above the form.\nconst summaryMobileOrder: CSSProperties = { order: -1 }\n// 快捷支付按钮里的品牌图形按字高缩放。\nconst brandLogo: CSSProperties = { height: 'var(--astryx-spacing-5)', width: 'auto' }\n// 卡组织图标是带描边的圆角小牌子。\nconst cardLogo: CSSProperties = {\n height: 'var(--astryx-spacing-7)',\n width: 'auto',\n borderRadius: 'var(--astryx-radius-element)',\n borderWidth: 'var(--astryx-border-width)',\n borderStyle: 'solid',\n borderColor: 'var(--astryx-color-border)',\n backgroundColor: 'var(--astryx-color-background-surface)',\n}\nconst isMobile = useMediaQuery('(max-width: 767px)')\nconst firstName = ref('')\nconst lastName = ref('')\nconst address = ref('')\nconst city = ref('')\nconst zip = ref('')\nconst state = ref('')\nconst phone = ref('')\nconst saveInfo = ref(false)\nconst email = ref('')\nconst emailOffers = ref(false)\nconst deliveryMethod = ref('standard')\nconst paymentMethod = ref('card')\nconst cardNumber = ref('')\nconst expiry = ref('')\nconst expYear = ref('')\nconst billingMatchesShipping = ref(true)\nconst billingAddress = ref('')\nconst billingCity = ref('')\nconst billingZip = ref('')\nconst billingState = ref('')\nconst cvc = ref('')\nconst cardName = ref('')\nconst promo = ref('')\nconst addGiftMessage = ref(false)\nconst giftTo = ref('')\nconst giftFrom = ref('')\nconst giftMessage = ref('')\nconst quantities = ref<Record<string, number>>({ 1: 1, 2: 1, 3: 1 })\nconst submitted = ref(false)\n\nconst shippingCost = computed(() => deliveryMethod.value === 'expedited' ? 9.95 : 4.95)\nconst orderTotal = computed(() => SUBTOTAL + shippingCost.value + TAX)\n\nconst errors = computed(() => submitted.value\n ? {\n firstName: !firstName.value.trim() ? 'Required' : undefined,\n lastName: !lastName.value.trim() ? 'Required' : undefined,\n address: !address.value.trim() ? 'Required' : undefined,\n city: !city.value.trim() ? 'Required' : undefined,\n zip: !zip.value.trim() ? 'Required' : undefined,\n state: !state.value ? 'Required' : undefined,\n email: !email.value.trim() ? 'Required' : undefined,\n phone: !phone.value.trim() ? 'Required' : undefined,\n expiry: !expiry.value ? 'Required' : undefined,\n expYear: !expYear.value ? 'Required' : undefined,\n cvc: !cvc.value.trim() ? 'Required' : undefined,\n cardNumber: paymentMethod.value === 'card' && !cardNumber.value.trim() ? 'Required' : undefined,\n cardName: paymentMethod.value === 'card' && !cardName.value.trim() ? 'Required' : undefined,\n billingAddress: !billingMatchesShipping.value && !billingAddress.value.trim() ? 'Required' : undefined,\n billingCity: !billingMatchesShipping.value && !billingCity.value.trim() ? 'Required' : undefined,\n billingZip: !billingMatchesShipping.value && !billingZip.value.trim() ? 'Required' : undefined,\n billingState: !billingMatchesShipping.value && !billingState.value ? 'Required' : undefined,\n }\n : {})\n<\/script>\n\n<template>\n <Layout height=\"fill\">\n <template #default>\n <LayoutContent :padding=\"0\">\n <Center axis=\"horizontal\">\n <Section variant=\"transparent\" :max-width=\"1100\" width=\"100%\" :padding=\"6\">\n <VStack :gap=\"5\">\n <VStack :gap=\"6\">\n <VStack :gap=\"2\">\n <Text type=\"display-1\" as=\"h1\">\n Payment Request\n </Text>\n <Text type=\"body\" color=\"secondary\">\n Review your order and complete your purchase. All transactions are secured with 256-bit SSL encryption.\n </Text>\n </VStack>\n <Divider />\n </VStack>\n <Stack :direction=\"isMobile ? 'vertical' : 'horizontal'\" :gap=\"8\" align=\"start\">\n <StackItem size=\"fill\" :style=\"isMobile ? undefined : formColBasis\">\n <VStack :gap=\"8\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Sign in to check out\n </Text>\n <Button label=\"Sign In\" variant=\"secondary\" size=\"sm\" />\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n Sign in to track your order and save your information for faster checkout.\n </Text>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Contact Information\n </Text>\n <TextInput v-model=\"email\" size=\"lg\" label=\"Email\" placeholder=\"you@example.com\" :status=\"errors.email ? { type: 'error', message: errors.email } : undefined\" />\n <CheckboxInput v-model=\"emailOffers\" label=\"Email me with news and offers\" />\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Shipping Information\n </Text>\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"firstName\" size=\"lg\" label=\"First Name\" placeholder=\"John\" :status=\"errors.firstName ? { type: 'error', message: errors.firstName } : undefined\" />\n <TextInput v-model=\"lastName\" size=\"lg\" label=\"Last Name\" placeholder=\"Doe\" :status=\"errors.lastName ? { type: 'error', message: errors.lastName } : undefined\" />\n </Grid>\n <TextInput v-model=\"address\" size=\"lg\" label=\"Address\" placeholder=\"123 Main Street\" :status=\"errors.address ? { type: 'error', message: errors.address } : undefined\" />\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"city\" size=\"lg\" label=\"City\" placeholder=\"New York\" :status=\"errors.city ? { type: 'error', message: errors.city } : undefined\" />\n <TextInput v-model=\"zip\" size=\"lg\" label=\"ZIP Code\" placeholder=\"10001\" :status=\"errors.zip ? { type: 'error', message: errors.zip } : undefined\" />\n </Grid>\n <Selector v-model=\"state\" size=\"lg\" label=\"State\" placeholder=\"Select state\" :options=\"US_STATES\" :status=\"errors.state ? { type: 'error', message: errors.state } : undefined\" />\n <TextInput v-model=\"phone\" size=\"lg\" label=\"Phone Number\" placeholder=\"+1 (555) 123-4567\" label-tooltip=\"We use your phone number to provide shipping updates and contact you about your order if needed.\" :status=\"errors.phone ? { type: 'error', message: errors.phone } : undefined\" />\n <CheckboxInput v-model=\"saveInfo\" label=\"Save my information for a faster checkout\" />\n </VStack>\n <VStack :gap=\"3\">\n <VStack :gap=\"1\">\n <Text type=\"large\" weight=\"bold\">\n Delivery\n </Text>\n <Text type=\"supporting\" color=\"secondary\">\n Please allow 1–3 business days processing time before your order ships.\n </Text>\n </VStack>\n <RadioList v-model=\"deliveryMethod\" label=\"Delivery method\">\n <RadioListItem value=\"standard\" label=\"Standard (3–7 business days)\">\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n $4.95\n </Text>\n </template>\n </RadioListItem>\n <RadioListItem value=\"expedited\" label=\"Expedited (1–2 business days)\">\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n $9.95\n </Text>\n </template>\n </RadioListItem>\n </RadioList>\n </VStack>\n <VStack :gap=\"3\">\n <VStack :gap=\"1\">\n <Text type=\"large\" weight=\"bold\">\n Payment Method\n </Text>\n <Text type=\"supporting\" color=\"secondary\">\n All transactions are secure and encrypted.\n </Text>\n </VStack>\n <Grid :columns=\"2\" :gap=\"3\">\n <Button label=\"PayPal\" variant=\"primary\" size=\"lg\" :style=\"{ backgroundColor: '#FFC439', borderColor: '#FFC439' }\">\n <img src=\"https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-100px.png\" alt=\"PayPal\" :style=\"brandLogo\">\n </Button>\n <Button label=\"Google Pay\" variant=\"primary\" size=\"lg\" :style=\"{ backgroundColor: '#000', borderColor: '#000' }\">\n <img src=\"https://pay.google.com/about/static_kcs/images/logos/google-pay-logo.svg\" alt=\"Google Pay\" :style=\"brandLogo\">\n </Button>\n </Grid>\n <HStack :gap=\"3\" align=\"center\">\n <StackItem size=\"fill\">\n <Divider />\n </StackItem>\n <Text type=\"supporting\" color=\"secondary\">\n OR\n </Text>\n <StackItem size=\"fill\">\n <Divider />\n </StackItem>\n </HStack>\n <HStack :gap=\"1.5\" align=\"center\">\n <img v-for=\"logo in CARD_LOGOS\" :key=\"logo.alt\" :src=\"logo.src\" :alt=\"logo.alt\" :style=\"cardLogo\">\n </HStack>\n <TextInput v-model=\"cardNumber\" size=\"lg\" label=\"Card Number\" placeholder=\"1234 5678 9012 3456\" :status=\"errors.cardNumber ? { type: 'error', message: errors.cardNumber } : undefined\" />\n <Grid :columns=\"3\" :gap=\"3\">\n <Selector v-model=\"expiry\" size=\"lg\" label=\"Expiry Month\" placeholder=\"MM\" :options=\"MONTHS\" :status=\"errors.expiry ? { type: 'error', message: errors.expiry } : undefined\" />\n <Selector v-model=\"expYear\" size=\"lg\" label=\"Expiry Year\" placeholder=\"YY\" :options=\"YEARS\" :status=\"errors.expYear ? { type: 'error', message: errors.expYear } : undefined\" />\n <TextInput v-model=\"cvc\" size=\"lg\" label=\"CVC\" placeholder=\"123\" label-tooltip=\"3-digit security code usually found on the back of your card. American Express cards have a 4-digit code located on the front.\" :status=\"errors.cvc ? { type: 'error', message: errors.cvc } : undefined\" />\n </Grid>\n <TextInput v-model=\"cardName\" size=\"lg\" label=\"Name on Card\" placeholder=\"John Doe\" :status=\"errors.cardName ? { type: 'error', message: errors.cardName } : undefined\" />\n <CheckboxInput v-model=\"billingMatchesShipping\" label=\"Use shipping address as billing address\" />\n <VStack v-if=\"!billingMatchesShipping\" :gap=\"3\">\n <TextInput v-model=\"billingAddress\" size=\"lg\" label=\"Address\" placeholder=\"123 Main Street\" :status=\"errors.billingAddress ? { type: 'error', message: errors.billingAddress } : undefined\" />\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"billingCity\" size=\"lg\" label=\"City\" placeholder=\"New York\" :status=\"errors.billingCity ? { type: 'error', message: errors.billingCity } : undefined\" />\n <TextInput v-model=\"billingZip\" size=\"lg\" label=\"ZIP Code\" placeholder=\"10001\" :status=\"errors.billingZip ? { type: 'error', message: errors.billingZip } : undefined\" />\n </Grid>\n <Selector v-model=\"billingState\" size=\"lg\" label=\"State\" placeholder=\"Select state\" :options=\"US_STATES\" :status=\"errors.billingState ? { type: 'error', message: errors.billingState } : undefined\" />\n </VStack>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Promo Code\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <StackItem size=\"fill\">\n <TextInput v-model=\"promo\" size=\"lg\" label=\"Promo code\" is-label-hidden placeholder=\"Enter promo code\" :style=\"fullWidth\" />\n </StackItem>\n <Button label=\"Apply\" variant=\"secondary\" size=\"lg\" />\n </HStack>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Gift Options\n </Text>\n <CheckboxInput v-model=\"addGiftMessage\" label=\"Add a gift message\" />\n <VStack v-if=\"addGiftMessage\" :gap=\"3\">\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"giftTo\" size=\"lg\" label=\"To\" is-label-hidden placeholder=\"To\" />\n <TextInput v-model=\"giftFrom\" size=\"lg\" label=\"From\" is-label-hidden placeholder=\"From\" />\n </Grid>\n <TextArea v-model=\"giftMessage\" label=\"Gift message\" is-label-hidden placeholder=\"Write something here\" />\n </VStack>\n </VStack>\n <VStack :gap=\"4\">\n <HStack :gap=\"5\" justify=\"center\" wrap=\"wrap\">\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:shield-check\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Secure Payment\n </Text>\n </HStack>\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:lock-closed\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n SSL Encrypted\n </Text>\n </HStack>\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:check-circle\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Free Returns\n </Text>\n </HStack>\n </HStack>\n <VStack :gap=\"2\">\n <Button label=\"Place Order\" variant=\"primary\" size=\"lg\" :style=\"fullWidth\" @click=\"submitted = true\" />\n <Button label=\"Continue Shopping\" variant=\"secondary\" size=\"lg\" :style=\"fullWidth\" />\n </VStack>\n <Divider />\n <HStack :gap=\"4\" align=\"center\" wrap=\"wrap\">\n <Link v-for=\"policy in POLICIES\" :key=\"policy\" href=\"#\" size=\"sm\">\n {{ policy }}\n </Link>\n </HStack>\n </VStack>\n </VStack>\n </StackItem>\n <StackItem size=\"fill\" :style=\"isMobile ? summaryMobileOrder : summarySticky\">\n <Card :padding=\"5\">\n <Collapsible trigger=\"Order Summary\" :default-is-open=\"true\">\n <VStack :gap=\"4\" :style=\"summaryContent\">\n <VStack v-for=\"item in ORDER_ITEMS\" :key=\"item.id\" :gap=\"3\">\n <HStack :gap=\"3\" align=\"start\">\n <Thumbnail :src=\"ITEM_IMAGES[item.id].src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" justify=\"between\" align=\"start\">\n <HStack :gap=\"2\" align=\"center\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Badge v-if=\"item.limited\" variant=\"green\" label=\"LIMITED EDITION\" />\n </HStack>\n <Text type=\"body\" weight=\"bold\">\n {{ fmt(item.price) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }}\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <span class=\"quantity-field\">\n <NumberInput v-model=\"quantities[item.id]\" label=\"Qty\" is-label-hidden :min=\"1\" :max=\"10\" is-integer-only />\n </span>\n <Link size=\"sm\" href=\"#\" @click.prevent=\"quantities[item.id] = 0\">\n Remove\n </Link>\n <Link size=\"sm\" href=\"#\">\n Save\n </Link>\n </HStack>\n </VStack>\n </StackItem>\n </HStack>\n <Divider />\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Order Total\n </Text>\n <VStack :gap=\"2\">\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Subtotal\n </Text>\n <Text type=\"body\">\n {{ fmt(SUBTOTAL) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Shipping\n </Text>\n <Text type=\"body\">\n {{ fmt(shippingCost) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Tax\n </Text>\n <Text type=\"body\">\n {{ fmt(TAX) }}\n </Text>\n </HStack>\n </VStack>\n <Divider />\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Total\n </Text>\n <Text type=\"large\" weight=\"bold\">\n {{ fmt(orderTotal) }}\n </Text>\n </HStack>\n <Banner status=\"info\" title=\"Free shipping on orders over $300\">\n <template #icon>\n <Icon name=\"heroicons:truck\" size=\"sm\" />\n </template>\n </Banner>\n </VStack>\n </VStack>\n </Collapsible>\n </Card>\n </StackItem>\n </Stack>\n </VStack>\n </Section>\n </Center>\n </LayoutContent>\n </template>\n </Layout>\n</template>\n\n<style scoped>\n.quantity-field {\n display: block;\n flex: none;\n /* 待 core 修复后去掉:NumberInput 原生 input 默认宽度比原版窄 52px,数量框整体补宽到原版的 164。 */\n width: 164px;\n}\n</style>\n",
|
|
79650
|
+
"source": "<script setup lang=\"ts\">\nimport type { CSSProperties } from 'vue'\nimport {\n Badge,\n Banner,\n Button,\n Card,\n Center,\n CheckboxInput,\n Collapsible,\n Divider,\n Grid,\n HStack,\n Icon,\n Layout,\n LayoutContent,\n Link,\n NumberInput,\n RadioList,\n RadioListItem,\n Section,\n Selector,\n Stack,\n StackItem,\n Text,\n TextArea,\n TextInput,\n Thumbnail,\n useMediaQuery,\n VStack,\n} from '@astryx-vue/core'\nimport { computed, ref } from 'vue'\n\n// ── Constants ─────────────────────────────────────────────────────────────────\n\nconst MONTHS = [\n '01',\n '02',\n '03',\n '04',\n '05',\n '06',\n '07',\n '08',\n '09',\n '10',\n '11',\n '12',\n]\nconst YEARS = Array.from({ length: 12 }, (_, i) => String(2025 + i))\n\nconst US_STATES = [\n 'Alabama',\n 'Alaska',\n 'Arizona',\n 'Arkansas',\n 'California',\n 'Colorado',\n 'Connecticut',\n 'Delaware',\n 'Florida',\n 'Georgia',\n 'Hawaii',\n 'Idaho',\n 'Illinois',\n 'Indiana',\n 'Iowa',\n 'Kansas',\n 'Kentucky',\n 'Louisiana',\n 'Maine',\n 'Maryland',\n 'Massachusetts',\n 'Michigan',\n 'Minnesota',\n 'Mississippi',\n 'Missouri',\n 'Montana',\n 'Nebraska',\n 'Nevada',\n 'New Hampshire',\n 'New Jersey',\n 'New Mexico',\n 'New York',\n 'North Carolina',\n 'North Dakota',\n 'Ohio',\n 'Oklahoma',\n 'Oregon',\n 'Pennsylvania',\n 'Rhode Island',\n 'South Carolina',\n 'South Dakota',\n 'Tennessee',\n 'Texas',\n 'Utah',\n 'Vermont',\n 'Virginia',\n 'Washington',\n 'West Virginia',\n 'Wisconsin',\n 'Wyoming',\n]\n\n// Product photos from the local template-assets set (committed to the\n// docsite; the CLI swaps these for an inline placeholder on scaffold).\nconst ITEM_IMAGES: Record<string, { src: string }> = {\n 1: {\n src: '/template-assets/light-product-1.png',\n },\n 2: {\n src: '/template-assets/light-product-4.png',\n },\n 3: {\n src: '/template-assets/light-product-5.png',\n },\n}\n\nconst ORDER_ITEMS = [\n {\n id: '1',\n name: 'Speckled Stoneware Mug',\n variant: 'Hand-thrown · 12 oz',\n price: 78,\n qty: 1,\n limited: false,\n },\n {\n id: '2',\n name: 'Stoneware Dinner Plate',\n variant: 'Reactive glaze · 10 in',\n price: 72,\n qty: 1,\n limited: false,\n },\n {\n id: '3',\n name: 'Cereal Bowl',\n variant: 'Speckled clay · 6 in',\n price: 80,\n qty: 1,\n limited: true,\n },\n]\n\nconst SUBTOTAL = 230\n// SHIPPING is now computed from deliveryMethod state\nconst TAX = 18.4\n// TOTAL is computed dynamically based on delivery selection\nconst fmt = (n: number) => `$${n.toFixed(2)}`\n\nconst CARD_LOGOS = [\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/visa.svg', alt: 'Visa' },\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/mastercard.svg', alt: 'Mastercard' },\n { src: 'https://raw.githubusercontent.com/aaronfagan/svg-credit-card-payment-icons/main/flat/amex.svg', alt: 'Amex' },\n]\n\nconst POLICIES = ['Refund policy', 'Privacy policy', 'Terms of service', 'Cancellations']\n\n// ── Styles ────────────────────────────────────────────────────────────────────\n// Plain inline styles using Astryx design-token CSS variables (declared at\n// :root by `@astryxdesign/core/astryx.css`). No StyleX compiler required.\n\nconst fullWidth: CSSProperties = { width: '100%' }\n// Form column flex-basis so the two checkout columns share width evenly.\nconst formColBasis: CSSProperties = { flexBasis: 0 }\n// Space the Order Summary content below its collapsible trigger title.\nconst summaryContent: CSSProperties = { paddingBlockStart: 'var(--astryx-spacing-2)' }\n// Order-summary column: sticky beside the form on desktop.\nconst summarySticky: CSSProperties = {\n flexBasis: 0,\n position: 'sticky',\n top: 'var(--astryx-spacing-4)',\n alignSelf: 'flex-start',\n}\n// On mobile the summary moves above the form.\nconst summaryMobileOrder: CSSProperties = { order: -1 }\n// 快捷支付按钮里的品牌图形按字高缩放。\nconst brandLogo: CSSProperties = { height: 'var(--astryx-spacing-5)', width: 'auto' }\n// 卡组织图标是带描边的圆角小牌子。\nconst cardLogo: CSSProperties = {\n height: 'var(--astryx-spacing-7)',\n width: 'auto',\n borderRadius: 'var(--astryx-radius-element)',\n borderWidth: 'var(--astryx-border-width)',\n borderStyle: 'solid',\n borderColor: 'var(--astryx-color-border)',\n backgroundColor: 'var(--astryx-color-background-surface)',\n}\nconst isMobile = useMediaQuery('(max-width: 767px)')\nconst firstName = ref('')\nconst lastName = ref('')\nconst address = ref('')\nconst city = ref('')\nconst zip = ref('')\nconst state = ref('')\nconst phone = ref('')\nconst saveInfo = ref(false)\nconst email = ref('')\nconst emailOffers = ref(false)\nconst deliveryMethod = ref('standard')\nconst paymentMethod = ref('card')\nconst cardNumber = ref('')\nconst expiry = ref('')\nconst expYear = ref('')\nconst billingMatchesShipping = ref(true)\nconst billingAddress = ref('')\nconst billingCity = ref('')\nconst billingZip = ref('')\nconst billingState = ref('')\nconst cvc = ref('')\nconst cardName = ref('')\nconst promo = ref('')\nconst addGiftMessage = ref(false)\nconst giftTo = ref('')\nconst giftFrom = ref('')\nconst giftMessage = ref('')\nconst quantities = ref<Record<string, number>>({ 1: 1, 2: 1, 3: 1 })\nconst submitted = ref(false)\n\nconst shippingCost = computed(() => deliveryMethod.value === 'expedited' ? 9.95 : 4.95)\nconst orderTotal = computed(() => SUBTOTAL + shippingCost.value + TAX)\n\nconst errors = computed(() => submitted.value\n ? {\n firstName: !firstName.value.trim() ? 'Required' : undefined,\n lastName: !lastName.value.trim() ? 'Required' : undefined,\n address: !address.value.trim() ? 'Required' : undefined,\n city: !city.value.trim() ? 'Required' : undefined,\n zip: !zip.value.trim() ? 'Required' : undefined,\n state: !state.value ? 'Required' : undefined,\n email: !email.value.trim() ? 'Required' : undefined,\n phone: !phone.value.trim() ? 'Required' : undefined,\n expiry: !expiry.value ? 'Required' : undefined,\n expYear: !expYear.value ? 'Required' : undefined,\n cvc: !cvc.value.trim() ? 'Required' : undefined,\n cardNumber: paymentMethod.value === 'card' && !cardNumber.value.trim() ? 'Required' : undefined,\n cardName: paymentMethod.value === 'card' && !cardName.value.trim() ? 'Required' : undefined,\n billingAddress: !billingMatchesShipping.value && !billingAddress.value.trim() ? 'Required' : undefined,\n billingCity: !billingMatchesShipping.value && !billingCity.value.trim() ? 'Required' : undefined,\n billingZip: !billingMatchesShipping.value && !billingZip.value.trim() ? 'Required' : undefined,\n billingState: !billingMatchesShipping.value && !billingState.value ? 'Required' : undefined,\n }\n : {})\n<\/script>\n\n<template>\n <Layout height=\"fill\">\n <template #default>\n <LayoutContent :padding=\"0\">\n <Center axis=\"horizontal\">\n <Section variant=\"transparent\" :max-width=\"1100\" width=\"100%\" :padding=\"6\">\n <VStack :gap=\"5\">\n <VStack :gap=\"6\">\n <VStack :gap=\"2\">\n <Text type=\"display-1\" as=\"h1\">\n Payment Request\n </Text>\n <Text type=\"body\" color=\"secondary\">\n Review your order and complete your purchase. All transactions are secured with 256-bit SSL encryption.\n </Text>\n </VStack>\n <Divider />\n </VStack>\n <Stack :direction=\"isMobile ? 'vertical' : 'horizontal'\" :gap=\"8\" align=\"start\">\n <StackItem size=\"fill\" :style=\"isMobile ? undefined : formColBasis\">\n <VStack :gap=\"8\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Sign in to check out\n </Text>\n <Button label=\"Sign In\" variant=\"secondary\" size=\"sm\" />\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n Sign in to track your order and save your information for faster checkout.\n </Text>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Contact Information\n </Text>\n <TextInput v-model=\"email\" size=\"lg\" label=\"Email\" placeholder=\"you@example.com\" :status=\"errors.email ? { type: 'error', message: errors.email } : undefined\" />\n <CheckboxInput v-model=\"emailOffers\" label=\"Email me with news and offers\" />\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Shipping Information\n </Text>\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"firstName\" size=\"lg\" label=\"First Name\" placeholder=\"John\" :status=\"errors.firstName ? { type: 'error', message: errors.firstName } : undefined\" />\n <TextInput v-model=\"lastName\" size=\"lg\" label=\"Last Name\" placeholder=\"Doe\" :status=\"errors.lastName ? { type: 'error', message: errors.lastName } : undefined\" />\n </Grid>\n <TextInput v-model=\"address\" size=\"lg\" label=\"Address\" placeholder=\"123 Main Street\" :status=\"errors.address ? { type: 'error', message: errors.address } : undefined\" />\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"city\" size=\"lg\" label=\"City\" placeholder=\"New York\" :status=\"errors.city ? { type: 'error', message: errors.city } : undefined\" />\n <TextInput v-model=\"zip\" size=\"lg\" label=\"ZIP Code\" placeholder=\"10001\" :status=\"errors.zip ? { type: 'error', message: errors.zip } : undefined\" />\n </Grid>\n <Selector v-model=\"state\" size=\"lg\" label=\"State\" placeholder=\"Select state\" :options=\"US_STATES\" :status=\"errors.state ? { type: 'error', message: errors.state } : undefined\" />\n <TextInput v-model=\"phone\" size=\"lg\" label=\"Phone Number\" placeholder=\"+1 (555) 123-4567\" label-tooltip=\"We use your phone number to provide shipping updates and contact you about your order if needed.\" :status=\"errors.phone ? { type: 'error', message: errors.phone } : undefined\" />\n <CheckboxInput v-model=\"saveInfo\" label=\"Save my information for a faster checkout\" />\n </VStack>\n <VStack :gap=\"3\">\n <VStack :gap=\"1\">\n <Text type=\"large\" weight=\"bold\">\n Delivery\n </Text>\n <Text type=\"supporting\" color=\"secondary\">\n Please allow 1–3 business days processing time before your order ships.\n </Text>\n </VStack>\n <RadioList v-model=\"deliveryMethod\" label=\"Delivery method\">\n <RadioListItem value=\"standard\" label=\"Standard (3–7 business days)\">\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n $4.95\n </Text>\n </template>\n </RadioListItem>\n <RadioListItem value=\"expedited\" label=\"Expedited (1–2 business days)\">\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n $9.95\n </Text>\n </template>\n </RadioListItem>\n </RadioList>\n </VStack>\n <VStack :gap=\"3\">\n <VStack :gap=\"1\">\n <Text type=\"large\" weight=\"bold\">\n Payment Method\n </Text>\n <Text type=\"supporting\" color=\"secondary\">\n All transactions are secure and encrypted.\n </Text>\n </VStack>\n <Grid :columns=\"2\" :gap=\"3\">\n <Button label=\"PayPal\" variant=\"primary\" size=\"lg\" :style=\"{ backgroundColor: '#FFC439', borderColor: '#FFC439' }\">\n <img src=\"https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-100px.png\" alt=\"PayPal\" :style=\"brandLogo\">\n </Button>\n <Button label=\"Google Pay\" variant=\"primary\" size=\"lg\" :style=\"{ backgroundColor: 'white', borderColor: 'white' }\">\n <img src=\"https://pay.google.com/about/static_kcs/images/logos/google-pay-logo.svg\" alt=\"Google Pay\" :style=\"brandLogo\">\n </Button>\n </Grid>\n <HStack :gap=\"3\" align=\"center\">\n <StackItem size=\"fill\">\n <Divider />\n </StackItem>\n <Text type=\"supporting\" color=\"secondary\">\n OR\n </Text>\n <StackItem size=\"fill\">\n <Divider />\n </StackItem>\n </HStack>\n <HStack :gap=\"1.5\" align=\"center\">\n <img v-for=\"logo in CARD_LOGOS\" :key=\"logo.alt\" :src=\"logo.src\" :alt=\"logo.alt\" :style=\"cardLogo\">\n </HStack>\n <TextInput v-model=\"cardNumber\" size=\"lg\" label=\"Card Number\" placeholder=\"1234 5678 9012 3456\" :status=\"errors.cardNumber ? { type: 'error', message: errors.cardNumber } : undefined\" />\n <Grid :columns=\"3\" :gap=\"3\">\n <Selector v-model=\"expiry\" size=\"lg\" label=\"Expiry Month\" placeholder=\"MM\" :options=\"MONTHS\" :status=\"errors.expiry ? { type: 'error', message: errors.expiry } : undefined\" />\n <Selector v-model=\"expYear\" size=\"lg\" label=\"Expiry Year\" placeholder=\"YY\" :options=\"YEARS\" :status=\"errors.expYear ? { type: 'error', message: errors.expYear } : undefined\" />\n <TextInput v-model=\"cvc\" size=\"lg\" label=\"CVC\" placeholder=\"123\" label-tooltip=\"3-digit security code usually found on the back of your card. American Express cards have a 4-digit code located on the front.\" :status=\"errors.cvc ? { type: 'error', message: errors.cvc } : undefined\" />\n </Grid>\n <TextInput v-model=\"cardName\" size=\"lg\" label=\"Name on Card\" placeholder=\"John Doe\" :status=\"errors.cardName ? { type: 'error', message: errors.cardName } : undefined\" />\n <CheckboxInput v-model=\"billingMatchesShipping\" label=\"Use shipping address as billing address\" />\n <VStack v-if=\"!billingMatchesShipping\" :gap=\"3\">\n <TextInput v-model=\"billingAddress\" size=\"lg\" label=\"Address\" placeholder=\"123 Main Street\" :status=\"errors.billingAddress ? { type: 'error', message: errors.billingAddress } : undefined\" />\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"billingCity\" size=\"lg\" label=\"City\" placeholder=\"New York\" :status=\"errors.billingCity ? { type: 'error', message: errors.billingCity } : undefined\" />\n <TextInput v-model=\"billingZip\" size=\"lg\" label=\"ZIP Code\" placeholder=\"10001\" :status=\"errors.billingZip ? { type: 'error', message: errors.billingZip } : undefined\" />\n </Grid>\n <Selector v-model=\"billingState\" size=\"lg\" label=\"State\" placeholder=\"Select state\" :options=\"US_STATES\" :status=\"errors.billingState ? { type: 'error', message: errors.billingState } : undefined\" />\n </VStack>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Promo Code\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <StackItem size=\"fill\">\n <TextInput v-model=\"promo\" size=\"lg\" label=\"Promo code\" is-label-hidden placeholder=\"Enter promo code\" :style=\"fullWidth\" />\n </StackItem>\n <Button label=\"Apply\" variant=\"secondary\" size=\"lg\" />\n </HStack>\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Gift Options\n </Text>\n <CheckboxInput v-model=\"addGiftMessage\" label=\"Add a gift message\" />\n <VStack v-if=\"addGiftMessage\" :gap=\"3\">\n <Grid :columns=\"2\" :gap=\"3\">\n <TextInput v-model=\"giftTo\" size=\"lg\" label=\"To\" is-label-hidden placeholder=\"To\" />\n <TextInput v-model=\"giftFrom\" size=\"lg\" label=\"From\" is-label-hidden placeholder=\"From\" />\n </Grid>\n <TextArea v-model=\"giftMessage\" label=\"Gift message\" is-label-hidden placeholder=\"Write something here\" />\n </VStack>\n </VStack>\n <VStack :gap=\"4\">\n <HStack :gap=\"5\" justify=\"center\" wrap=\"wrap\">\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:shield-check\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Secure Payment\n </Text>\n </HStack>\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:lock-closed\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n SSL Encrypted\n </Text>\n </HStack>\n <HStack :gap=\"1\" align=\"center\">\n <Icon name=\"heroicons:check-circle\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Free Returns\n </Text>\n </HStack>\n </HStack>\n <VStack :gap=\"2\">\n <Button label=\"Place Order\" variant=\"primary\" size=\"lg\" :style=\"fullWidth\" @click=\"submitted = true\" />\n <Button label=\"Continue Shopping\" variant=\"secondary\" size=\"lg\" :style=\"fullWidth\" />\n </VStack>\n <Divider />\n <HStack :gap=\"4\" align=\"center\" wrap=\"wrap\">\n <Link v-for=\"policy in POLICIES\" :key=\"policy\" href=\"#\" size=\"sm\">\n {{ policy }}\n </Link>\n </HStack>\n </VStack>\n </VStack>\n </StackItem>\n <StackItem size=\"fill\" :style=\"isMobile ? summaryMobileOrder : summarySticky\">\n <Card :padding=\"5\">\n <Collapsible trigger=\"Order Summary\" :default-is-open=\"true\">\n <VStack :gap=\"4\" :style=\"summaryContent\">\n <VStack v-for=\"item in ORDER_ITEMS\" :key=\"item.id\" :gap=\"3\">\n <HStack :gap=\"3\" align=\"start\">\n <Thumbnail :src=\"ITEM_IMAGES[item.id].src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" justify=\"between\" align=\"start\">\n <HStack :gap=\"2\" align=\"center\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Badge v-if=\"item.limited\" variant=\"green\" label=\"LIMITED EDITION\" />\n </HStack>\n <Text type=\"body\" weight=\"bold\">\n {{ fmt(item.price) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }}\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <span class=\"quantity-field\">\n <NumberInput v-model=\"quantities[item.id]\" label=\"Qty\" is-label-hidden :min=\"1\" :max=\"10\" is-integer-only />\n </span>\n <Link size=\"sm\" href=\"#\" @click.prevent=\"quantities[item.id] = 0\">\n Remove\n </Link>\n <Link size=\"sm\" href=\"#\">\n Save\n </Link>\n </HStack>\n </VStack>\n </StackItem>\n </HStack>\n <Divider />\n </VStack>\n <VStack :gap=\"3\">\n <Text type=\"large\" weight=\"bold\">\n Order Total\n </Text>\n <VStack :gap=\"2\">\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Subtotal\n </Text>\n <Text type=\"body\">\n {{ fmt(SUBTOTAL) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Shipping\n </Text>\n <Text type=\"body\">\n {{ fmt(shippingCost) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Tax\n </Text>\n <Text type=\"body\">\n {{ fmt(TAX) }}\n </Text>\n </HStack>\n </VStack>\n <Divider />\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Total\n </Text>\n <Text type=\"large\" weight=\"bold\">\n {{ fmt(orderTotal) }}\n </Text>\n </HStack>\n <Banner status=\"info\" title=\"Free shipping on orders over $300\">\n <template #icon>\n <Icon name=\"heroicons:truck\" size=\"sm\" />\n </template>\n </Banner>\n </VStack>\n </VStack>\n </Collapsible>\n </Card>\n </StackItem>\n </Stack>\n </VStack>\n </Section>\n </Center>\n </LayoutContent>\n </template>\n </Layout>\n</template>\n\n<style scoped>\n.quantity-field {\n display: block;\n flex: none;\n /* 待 core 修复后去掉:NumberInput 原生 input 默认宽度比原版窄 52px,数量框整体补宽到原版的 164。 */\n width: 164px;\n}\n</style>\n",
|
|
77839
79651
|
"docsUrl": {
|
|
77840
79652
|
"en": "https://astryx-vue.pages.dev/en/playground/?template=payment-form",
|
|
77841
79653
|
"zh": "https://astryx-vue.pages.dev/playground/?template=payment-form"
|
|
@@ -77909,7 +79721,7 @@ const catalog = {
|
|
|
77909
79721
|
],
|
|
77910
79722
|
"dependencies": ["@astryx-vue/core", "vue"],
|
|
77911
79723
|
"isReady": true,
|
|
77912
|
-
"source": "<script setup lang=\"ts\">\nimport type { ComponentPublicInstance } from 'vue'\nimport {\n Badge,\n Banner,\n Button,\n Card,\n CheckboxInput,\n Collapsible,\n Divider,\n FieldStatus,\n FormLayout,\n Heading,\n HStack,\n Icon,\n Layout,\n LayoutContent,\n NumberInput,\n RadioList,\n RadioListItem,\n Selector,\n Stack,\n StackItem,\n Step,\n Stepper,\n Text,\n TextArea,\n TextInput,\n Thumbnail,\n VStack,\n} from '@astryx-vue/core'\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\n\n// ── 数据 ──────────────────────────────────────────────────────────────────────\n\nconst STEPS = [\n { label: 'Cart', icon: 'heroicons:shopping-cart' },\n { label: 'Shipping', icon: 'heroicons:map-pin' },\n { label: 'Delivery', icon: 'heroicons:truck' },\n { label: 'Payment', icon: 'heroicons:credit-card' },\n]\n\nconst CART_ITEMS = [\n { id: 'mug', name: 'Speckled Stoneware Mug', variant: 'Hand-thrown · 12 oz', price: 78, qty: 2, src: '/template-assets/light-product-1.png', isLimited: false },\n { id: 'plate', name: 'Stoneware Dinner Plate', variant: 'Reactive glaze · 10 in', price: 72, qty: 1, src: '/template-assets/light-product-4.png', isLimited: false },\n { id: 'bowl', name: 'Cereal Bowl', variant: 'Speckled clay · 6 in', price: 80, qty: 1, src: '/template-assets/light-product-5.png', isLimited: true },\n]\n\nconst COUNTRIES = [\n { value: 'us', label: 'United States' },\n { value: 'ca', label: 'Canada' },\n { value: 'uk', label: 'United Kingdom' },\n]\n\nconst US_STATES = ['California', 'Illinois', 'Massachusetts', 'New York', 'Oregon', 'Texas', 'Washington']\n\nconst DELIVERY_OPTIONS = [\n { value: 'standard', label: 'Standard', window: '3–7 business days', price: 4.95 },\n { value: 'expedited', label: 'Expedited', window: '1–2 business days', price: 9.95 },\n { value: 'overnight', label: 'Overnight', window: 'Next business day by 5pm', price: 24.95 },\n]\n\nconst PROMOS: Record<string, { label: string, rate: number }> = {\n WELCOME10: { label: 'WELCOME10 · 10% off', rate: 0.1 },\n CLAY20: { label: 'CLAY20 · 20% off', rate: 0.2 },\n}\n\nconst TAX_RATE = 0.08\nconst FREE_SHIPPING_THRESHOLD = 300\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@][^\\s.@]*\\.[^\\s@]{2,}$/\n\nconst POSTAL_CODE_RULES: Record<string, { label: string, placeholder: string, pattern: RegExp, error: string }> = {\n us: { label: 'ZIP code', placeholder: '10001', pattern: /^\\d{5}(?:-\\d{4})?$/, error: 'Enter a 5-digit ZIP code, with an optional 4-digit suffix.' },\n ca: { label: 'Postal code', placeholder: 'K1A 0B1', pattern: /^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][ -]?\\d[ABCEGHJ-NPRSTV-Z]\\d$/i, error: 'Enter a Canadian postal code, for example K1A 0B1.' },\n uk: { label: 'Postcode', placeholder: 'SW1A 1AA', pattern: /^(?:GIR\\s?0AA|[A-Z]{1,2}\\d[A-Z\\d]?\\s*\\d[A-Z]{2})$/i, error: 'Enter a UK postcode, for example SW1A 1AA.' },\n}\n\nfunction money(n: number) {\n return `$${n.toFixed(2)}`\n}\n\n// ── 状态 ──────────────────────────────────────────────────────────────────────\n\nconst NARROW_HOST_WIDTH = 900\nconst hostRef = ref<ComponentPublicInstance | null>(null)\nconst isNarrow = ref(false)\nlet hostObserver: ResizeObserver | undefined\n\n// 窄版按预览容器的宽度判断,而不是浏览器窗口宽度:嵌在窄预览里的页面窗口再宽也要走窄版。\nwatch(hostRef, (instance) => {\n hostObserver?.disconnect()\n const host = instance?.$el as HTMLElement | undefined\n if (!host)\n return\n if (host.clientWidth > 0)\n isNarrow.value = host.clientWidth <= NARROW_HOST_WIDTH\n hostObserver = new ResizeObserver((entries) => {\n const width = entries[0]?.contentRect.width ?? host.clientWidth\n if (width > 0)\n isNarrow.value = width <= NARROW_HOST_WIDTH\n })\n hostObserver.observe(host)\n})\nonBeforeUnmount(() => hostObserver?.disconnect())\n\nconst step = ref(0)\nconst attempted = ref<ReadonlySet<number>>(new Set())\nconst isComplete = ref(false)\n\nconst quantities = ref<Record<string, number>>(Object.fromEntries(CART_ITEMS.map(item => [item.id, item.qty])))\nconst promoInput = ref('')\nconst promoWasChecked = ref(false)\nconst appliedPromo = ref<string | null>(null)\n\nconst email = ref('')\nconst firstName = ref('')\nconst lastName = ref('')\nconst address = ref('')\nconst apartment = ref('')\nconst city = ref('')\nconst state = ref('')\nconst postalCode = ref('')\nconst country = ref('us')\n\nconst delivery = ref('standard')\nconst deliveryNote = ref('')\nconst isGift = ref(false)\n\nconst cardNumber = ref('')\nconst cardName = ref('')\nconst expiry = ref('')\nconst cvc = ref('')\nconst billingMatches = ref(true)\n\nconst items = computed(() =>\n CART_ITEMS\n .map(item => ({ ...item, qty: quantities.value[item.id] ?? item.qty }))\n .filter(item => item.qty > 0),\n)\n\nconst subtotal = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0))\nconst promo = computed(() => (appliedPromo.value ? PROMOS[appliedPromo.value] : undefined))\nconst discount = computed(() => (promo.value ? subtotal.value * promo.value.rate : 0))\nconst deliveryOption = computed(() => DELIVERY_OPTIONS.find(o => o.value === delivery.value) ?? DELIVERY_OPTIONS[0])\nconst shipping = computed(() =>\n subtotal.value - discount.value >= FREE_SHIPPING_THRESHOLD && delivery.value === 'standard'\n ? 0\n : deliveryOption.value.price,\n)\nconst tax = computed(() => (subtotal.value - discount.value) * TAX_RATE)\nconst total = computed(() => subtotal.value - discount.value + shipping.value + tax.value)\nconst itemCount = computed(() => items.value.reduce((sum, item) => sum + item.qty, 0))\n\nconst postalRule = computed(() => POSTAL_CODE_RULES[country.value] ?? POSTAL_CODE_RULES.us)\n\nconst errorsByStep = computed<Array<Record<string, string>>>(() => {\n const cart: Record<string, string> = {}\n if (items.value.length === 0)\n cart.items = 'Your cart is empty.'\n\n const shippingStep: Record<string, string> = {}\n if (!EMAIL_PATTERN.test(email.value))\n shippingStep.email = 'Enter an email address for order updates.'\n if (!firstName.value.trim())\n shippingStep.firstName = 'Required'\n if (!lastName.value.trim())\n shippingStep.lastName = 'Required'\n if (!address.value.trim())\n shippingStep.address = 'Required'\n if (!city.value.trim())\n shippingStep.city = 'Required'\n if (country.value === 'us' && !state.value)\n shippingStep.state = 'Required'\n if (!postalRule.value.pattern.test(postalCode.value.trim()))\n shippingStep.postalCode = postalRule.value.error\n\n const deliveryStep: Record<string, string> = {}\n if (delivery.value === 'overnight' && country.value !== 'us')\n deliveryStep.delivery = 'Overnight delivery is only available in the US.'\n\n const payment: Record<string, string> = {}\n if (cardNumber.value.replace(/\\s/g, '').length < 15)\n payment.cardNumber = 'Enter the full card number.'\n if (!cardName.value.trim())\n payment.cardName = 'Enter the name printed on the card.'\n if (!/^\\d{2}\\s?\\/\\s?\\d{2}$/.test(expiry.value.trim()))\n payment.expiry = 'Use MM/YY.'\n if (!/^\\d{3,4}$/.test(cvc.value.trim()))\n payment.cvc = '3 or 4 digits.'\n\n return [cart, shippingStep, deliveryStep, payment]\n})\n\nfunction shownErrors(index: number): Record<string, string> {\n return attempted.value.has(index) ? errorsByStep.value[index] : {}\n}\n\nconst currentErrors = computed(() => shownErrors(step.value))\nconst isLastStep = computed(() => step.value === STEPS.length - 1)\n\nfunction markAttempted(indices: number[]) {\n const next = new Set(attempted.value)\n for (const index of indices) next.add(index)\n attempted.value = next\n}\n\nfunction goNext() {\n markAttempted([step.value])\n if (Object.keys(errorsByStep.value[step.value]).length > 0)\n return\n if (isLastStep.value) {\n isComplete.value = true\n return\n }\n step.value += 1\n}\n\n/** 往前跳时先检查中间的步骤,遇到第一个有问题的就停在那里。 */\nfunction goTo(index: number) {\n if (index <= step.value) {\n step.value = index\n return\n }\n const checked: number[] = []\n let firstInvalid: number | null = null\n for (let candidate = step.value; candidate < index; candidate += 1) {\n checked.push(candidate)\n if (Object.keys(errorsByStep.value[candidate]).length > 0) {\n firstInvalid = candidate\n break\n }\n }\n markAttempted(checked)\n step.value = firstInvalid ?? index\n}\n\nfunction onPromoInput(value: string) {\n promoInput.value = value\n promoWasChecked.value = false\n appliedPromo.value = null\n}\n\nfunction applyPromo() {\n const code = promoInput.value.trim().toUpperCase()\n promoWasChecked.value = code.length > 0\n appliedPromo.value = PROMOS[code] ? code : null\n}\n\nconst promoStatus = computed<{ type: 'success' | 'warning', message: string } | undefined>(() => {\n if (appliedPromo.value)\n return { type: 'success', message: `${PROMOS[appliedPromo.value].label} applied.` }\n if (promoInput.value.trim() && promoWasChecked.value)\n return { type: 'warning', message: 'That promo code is not valid.' }\n if (promoInput.value.trim())\n return { type: 'warning', message: 'Press Apply to check this code.' }\n return undefined\n})\n\nfunction blockedMessage(count: number) {\n return count === 1 ? 'One problem above needs fixing first.' : `${count} problems above need fixing first.`\n}\n\nfunction setQuantity(id: string, value: number | null) {\n quantities.value = { ...quantities.value, [id]: value ?? 0 }\n}\n\nfunction isFreeDelivery(optionValue: string) {\n return optionValue === 'standard' && subtotal.value - discount.value >= FREE_SHIPPING_THRESHOLD\n}\n<\/script>\n\n<template>\n <Layout height=\"fill\" :padding=\"6\" :content-width=\"1000\">\n <LayoutContent>\n <VStack ref=\"hostRef\" :gap=\"6\">\n <HStack :gap=\"3\" align=\"center\">\n <StackItem size=\"fill\">\n <Heading :level=\"1\">\n Checkout\n </Heading>\n </StackItem>\n <HStack :gap=\"1.5\" align=\"center\">\n <Icon name=\"heroicons:lock-closed\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Secure checkout\n </Text>\n </HStack>\n </HStack>\n\n <Stack :direction=\"isNarrow ? 'vertical' : 'horizontal'\" :gap=\"6\" align=\"start\">\n <StackItem size=\"fill\">\n <VStack :gap=\"6\">\n <Banner\n v-if=\"isComplete\"\n status=\"success\"\n title=\"Order confirmed\"\n :description=\"`This local example completed an order for ${money(total)}. No payment was sent.`\"\n />\n <VStack v-if=\"isComplete\" :gap=\"4\">\n <Heading :level=\"2\">\n Thanks for your order\n </Heading>\n <Text type=\"body\" color=\"secondary\">\n The completion is stored in this page's component state so the template ends in a real, reviewable outcome.\n </Text>\n </VStack>\n\n <Stepper\n v-if=\"!isComplete\"\n :active-step=\"step\"\n orientation=\"horizontal\"\n density=\"balanced\"\n label=\"Checkout progress\"\n @step-click=\"goTo\"\n >\n <Step\n v-for=\"({ label, icon }, i) in STEPS\"\n :key=\"label\"\n :step=\"i\"\n :label=\"label\"\n :status=\"Object.keys(shownErrors(i)).length ? 'error' : undefined\"\n >\n <template #icon>\n <Icon v-if=\"Object.keys(shownErrors(i)).length\" icon=\"error\" size=\"sm\" />\n <Icon v-else :name=\"icon\" size=\"sm\" />\n </template>\n </Step>\n </Stepper>\n\n <VStack v-if=\"!isComplete && step === 0\" :gap=\"5\">\n <Heading :level=\"2\">\n Review your cart\n </Heading>\n <VStack :gap=\"4\">\n <VStack v-for=\"item in items\" :key=\"item.id\" :gap=\"3\">\n <HStack :gap=\"4\" align=\"start\" wrap=\"wrap\">\n <Thumbnail :src=\"item.src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" align=\"start\" justify=\"between\">\n <HStack :gap=\"2\" align=\"center\" wrap=\"wrap\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Badge v-if=\"item.isLimited\" variant=\"green\" label=\"Limited\" />\n </HStack>\n <Text type=\"body\" weight=\"bold\">\n {{ money(item.price * item.qty) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }} · {{ money(item.price) }} each\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <span class=\"quantity-field\">\n <NumberInput\n :model-value=\"item.qty\"\n :label=\"`Quantity of ${item.name}`\"\n is-label-hidden\n :min=\"0\"\n :max=\"10\"\n is-integer-only\n has-number-steppers\n @update:model-value=\"setQuantity(item.id, $event)\"\n />\n </span>\n <Button label=\"Remove\" variant=\"ghost\" @click=\"setQuantity(item.id, 0)\" />\n </HStack>\n </VStack>\n </StackItem>\n </HStack>\n <Divider />\n </VStack>\n </VStack>\n <FieldStatus\n v-if=\"currentErrors.items\"\n type=\"error\"\n variant=\"detached\"\n :message=\"currentErrors.items\"\n />\n <HStack :gap=\"2\" align=\"end\">\n <StackItem size=\"fill\">\n <TextInput\n :model-value=\"promoInput\"\n label=\"Promo code\"\n placeholder=\"WELCOME10\"\n :status=\"promoStatus\"\n @update:model-value=\"onPromoInput($event)\"\n />\n </StackItem>\n <Button label=\"Apply\" variant=\"secondary\" @click=\"applyPromo\" />\n </HStack>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 1\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n Where should it go?\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n Delivery options and tax are calculated from this address.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <TextInput\n v-model=\"email\"\n label=\"Email\"\n placeholder=\"you@example.com\"\n description=\"Order confirmation and tracking go here.\"\n :status=\"currentErrors.email ? { type: 'error', message: currentErrors.email } : undefined\"\n />\n <FormLayout :direction=\"isNarrow ? 'vertical' : 'horizontal'\" default-optionality=\"required\">\n <TextInput\n v-model=\"firstName\"\n label=\"First name\"\n :status=\"currentErrors.firstName ? { type: 'error', message: currentErrors.firstName } : undefined\"\n />\n <TextInput\n v-model=\"lastName\"\n label=\"Last name\"\n :status=\"currentErrors.lastName ? { type: 'error', message: currentErrors.lastName } : undefined\"\n />\n </FormLayout>\n <Selector v-model=\"country\" label=\"Country\" :options=\"COUNTRIES\" />\n <TextInput\n v-model=\"address\"\n label=\"Street address\"\n placeholder=\"123 Main Street\"\n :status=\"currentErrors.address ? { type: 'error', message: currentErrors.address } : undefined\"\n />\n <TextInput v-model=\"apartment\" label=\"Apartment, suite, etc.\" is-optional />\n <FormLayout :direction=\"isNarrow ? 'vertical' : 'horizontal'\" default-optionality=\"required\">\n <TextInput\n v-model=\"city\"\n label=\"City\"\n :status=\"currentErrors.city ? { type: 'error', message: currentErrors.city } : undefined\"\n />\n <Selector\n v-if=\"country === 'us'\"\n v-model=\"state\"\n label=\"State\"\n placeholder=\"Select state\"\n :options=\"US_STATES\"\n searchable\n :status=\"currentErrors.state ? 'error' : undefined\"\n :status-message=\"currentErrors.state\"\n />\n <TextInput\n v-model=\"postalCode\"\n :label=\"postalRule.label\"\n :placeholder=\"postalRule.placeholder\"\n :status=\"currentErrors.postalCode ? { type: 'error', message: currentErrors.postalCode } : undefined\"\n />\n </FormLayout>\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 2\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n How fast do you need it?\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n Estimates run from the day the order ships, usually 1–3 business days after you place it.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <RadioList\n v-model=\"delivery\"\n label=\"Delivery method\"\n :status=\"currentErrors.delivery ? { type: 'error', message: currentErrors.delivery } : undefined\"\n >\n <RadioListItem\n v-for=\"option in DELIVERY_OPTIONS\"\n :key=\"option.value\"\n :value=\"option.value\"\n :label=\"option.label\"\n :description=\"option.window\"\n >\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n {{ isFreeDelivery(option.value) ? 'Free' : money(option.price) }}\n </Text>\n </template>\n </RadioListItem>\n </RadioList>\n <Divider />\n <CheckboxInput v-model=\"isGift\" label=\"This is a gift\" />\n <TextArea\n v-model=\"deliveryNote\"\n :label=\"isGift ? 'Gift message' : 'Delivery instructions'\"\n is-optional\n :rows=\"3\"\n :max-length=\"240\"\n :placeholder=\"isGift ? 'Happy birthday — hope these get plenty of use.' : 'Leave with the neighbour at number 14.'\"\n :description=\"isGift ? 'Printed on a card. Prices are left off the packing slip.' : 'Passed to the carrier where they support it.'\"\n />\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 3\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n Payment\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n This template validates payment details and completes locally. No card is charged.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <TextInput\n v-model=\"cardNumber\"\n label=\"Card number\"\n placeholder=\"4242 4242 4242 4242\"\n :status=\"currentErrors.cardNumber ? { type: 'error', message: currentErrors.cardNumber } : undefined\"\n />\n <TextInput\n v-model=\"cardName\"\n label=\"Name on card\"\n :status=\"currentErrors.cardName ? { type: 'error', message: currentErrors.cardName } : undefined\"\n />\n <HStack :gap=\"4\" wrap=\"wrap\">\n <TextInput\n v-model=\"expiry\"\n label=\"Expiry\"\n :width=\"140\"\n placeholder=\"MM/YY\"\n :status=\"currentErrors.expiry ? { type: 'error', message: currentErrors.expiry } : undefined\"\n />\n <TextInput\n v-model=\"cvc\"\n label=\"CVC\"\n :width=\"140\"\n placeholder=\"123\"\n label-tooltip=\"Three digits on the back of the card, or four on the front for American Express.\"\n :status=\"currentErrors.cvc ? { type: 'error', message: currentErrors.cvc } : undefined\"\n />\n </HStack>\n <Divider />\n <CheckboxInput v-model=\"billingMatches\" label=\"Billing address matches the shipping address\" />\n <Banner\n v-if=\"!billingMatches\"\n status=\"info\"\n title=\"Connect a billing-address step\"\n description=\"This local example does not collect a separate billing address or send payment. Add that flow with your payment provider.\"\n />\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete\" :gap=\"3\" h-align=\"start\">\n <FieldStatus\n v-if=\"Object.keys(currentErrors).length > 0\"\n type=\"error\"\n variant=\"detached\"\n :message=\"blockedMessage(Object.keys(currentErrors).length)\"\n />\n <HStack :gap=\"3\" align=\"center\" width=\"100%\">\n <Button\n :label=\"step === 0 ? 'Continue shopping' : 'Back'\"\n variant=\"secondary\"\n @click=\"step = Math.max(0, step - 1)\"\n />\n <StackItem size=\"fill\">\n <Button\n :label=\"isLastStep ? `Pay ${money(total)}` : 'Continue'\"\n variant=\"primary\"\n width=\"100%\"\n @click=\"goNext\"\n />\n </StackItem>\n </HStack>\n </VStack>\n </VStack>\n </StackItem>\n\n <StackItem :class=\"isNarrow ? 'summary-column summary-column--narrow' : 'summary-column'\">\n <Card :padding=\"5\">\n <component\n :is=\"isNarrow ? Collapsible : VStack\"\n v-bind=\"isNarrow ? { trigger: `Order summary · ${money(total)}`, defaultIsOpen: false } : { gap: 4 }\"\n >\n <HStack v-if=\"!isNarrow\" :gap=\"2\" align=\"center\" justify=\"between\" class=\"summary-title\">\n <Text type=\"label\">\n Order summary\n </Text>\n <Badge variant=\"neutral\" :label=\"`${itemCount} ${itemCount === 1 ? 'item' : 'items'}`\" />\n </HStack>\n <VStack :gap=\"4\" :padding-block-start=\"isNarrow ? 3 : undefined\">\n <VStack :gap=\"3\">\n <HStack v-for=\"item in items\" :key=\"item.id\" :gap=\"3\" align=\"start\">\n <Thumbnail :src=\"item.src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"0.5\">\n <HStack :gap=\"2\" align=\"start\" justify=\"between\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Text type=\"body\" weight=\"bold\">\n {{ money(item.price * item.qty) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }} · Qty {{ item.qty }}\n </Text>\n </VStack>\n </StackItem>\n </HStack>\n </VStack>\n <Divider />\n <VStack :gap=\"2\">\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Subtotal\n </Text>\n <Text type=\"body\">\n {{ money(subtotal) }}\n </Text>\n </HStack>\n <HStack v-if=\"promo\" justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n {{ promo.label }}\n </Text>\n <Text type=\"body\">\n −{{ money(discount) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n {{ deliveryOption.label }} delivery\n </Text>\n <Text type=\"body\">\n {{ shipping === 0 ? 'Free' : money(shipping) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Estimated tax\n </Text>\n <Text type=\"body\">\n {{ money(tax) }}\n </Text>\n </HStack>\n <Divider />\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Total\n </Text>\n <Text type=\"large\" weight=\"bold\">\n {{ money(total) }}\n </Text>\n </HStack>\n </VStack>\n <Banner\n v-if=\"subtotal - discount < FREE_SHIPPING_THRESHOLD\"\n status=\"info\"\n :title=\"`${money(FREE_SHIPPING_THRESHOLD - (subtotal - discount))} away from free standard shipping`\"\n />\n </VStack>\n </component>\n </Card>\n </StackItem>\n </Stack>\n </VStack>\n </LayoutContent>\n </Layout>\n</template>\n\n<style scoped>\n/* 摘要列固定 360 宽,表单滚过时贴在页面内边距处;窄版时排到表单前面、占满一行。 */\n.summary-column {\n position: sticky;\n top: var(--astryx-spacing-6);\n flex-shrink: 0;\n align-self: flex-start;\n width: 360px;\n}\n.summary-column--narrow {\n position: static;\n order: -1;\n align-self: stretch;\n width: auto;\n}\n.quantity-field {\n display: block;\n flex: none;\n width: 96px;\n}\n</style>\n",
|
|
79724
|
+
"source": "<script setup lang=\"ts\">\nimport type { ComponentPublicInstance } from 'vue'\nimport {\n Badge,\n Banner,\n Button,\n Card,\n CheckboxInput,\n Collapsible,\n Divider,\n FieldStatus,\n FormLayout,\n Heading,\n HStack,\n Icon,\n Layout,\n LayoutContent,\n NumberInput,\n RadioList,\n RadioListItem,\n Selector,\n Stack,\n StackItem,\n Step,\n Stepper,\n Text,\n TextArea,\n TextInput,\n Thumbnail,\n VStack,\n} from '@astryx-vue/core'\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\n\n// ── 数据 ──────────────────────────────────────────────────────────────────────\n\nconst STEPS = [\n { label: 'Cart', icon: 'heroicons:shopping-cart' },\n { label: 'Shipping', icon: 'heroicons:map-pin' },\n { label: 'Delivery', icon: 'heroicons:truck' },\n { label: 'Payment', icon: 'heroicons:credit-card' },\n]\n\nconst CART_ITEMS = [\n { id: 'mug', name: 'Speckled Stoneware Mug', variant: 'Hand-thrown · 12 oz', price: 78, qty: 2, src: '/template-assets/light-product-1.png', isLimited: false },\n { id: 'plate', name: 'Stoneware Dinner Plate', variant: 'Reactive glaze · 10 in', price: 72, qty: 1, src: '/template-assets/light-product-4.png', isLimited: false },\n { id: 'bowl', name: 'Cereal Bowl', variant: 'Speckled clay · 6 in', price: 80, qty: 1, src: '/template-assets/light-product-5.png', isLimited: true },\n]\n\nconst COUNTRIES = [\n { value: 'us', label: 'United States' },\n { value: 'ca', label: 'Canada' },\n { value: 'uk', label: 'United Kingdom' },\n]\n\nconst US_STATES = ['California', 'Illinois', 'Massachusetts', 'New York', 'Oregon', 'Texas', 'Washington']\n\nconst DELIVERY_OPTIONS = [\n { value: 'standard', label: 'Standard', window: '3–7 business days', price: 4.95 },\n { value: 'expedited', label: 'Expedited', window: '1–2 business days', price: 9.95 },\n { value: 'overnight', label: 'Overnight', window: 'Next business day by 5pm', price: 24.95 },\n]\n\nconst PROMOS: Record<string, { label: string, rate: number }> = {\n WELCOME10: { label: 'WELCOME10 · 10% off', rate: 0.1 },\n CLAY20: { label: 'CLAY20 · 20% off', rate: 0.2 },\n}\n\nconst TAX_RATE = 0.08\nconst FREE_SHIPPING_THRESHOLD = 300\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@][^\\s.@]*\\.[^\\s@]{2,}$/\n\nconst POSTAL_CODE_RULES: Record<string, { label: string, placeholder: string, pattern: RegExp, error: string }> = {\n us: { label: 'ZIP code', placeholder: '10001', pattern: /^\\d{5}(?:-\\d{4})?$/, error: 'Enter a 5-digit ZIP code, with an optional 4-digit suffix.' },\n ca: { label: 'Postal code', placeholder: 'K1A 0B1', pattern: /^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][ -]?\\d[ABCEGHJ-NPRSTV-Z]\\d$/i, error: 'Enter a Canadian postal code, for example K1A 0B1.' },\n uk: { label: 'Postcode', placeholder: 'SW1A 1AA', pattern: /^(?:GIR\\s?0AA|[A-Z]{1,2}\\d[A-Z\\d]?\\s*\\d[A-Z]{2})$/i, error: 'Enter a UK postcode, for example SW1A 1AA.' },\n}\n\nfunction money(n: number) {\n return `$${n.toFixed(2)}`\n}\n\n// ── 状态 ──────────────────────────────────────────────────────────────────────\n\nconst NARROW_HOST_WIDTH = 900\nconst hostRef = ref<ComponentPublicInstance | null>(null)\nconst isNarrow = ref(false)\nlet hostObserver: ResizeObserver | undefined\n\n// 窄版按预览容器的宽度判断,而不是浏览器窗口宽度:嵌在窄预览里的页面窗口再宽也要走窄版。\nwatch(hostRef, (instance) => {\n hostObserver?.disconnect()\n const host = instance?.$el as HTMLElement | undefined\n if (!host)\n return\n if (host.clientWidth > 0)\n isNarrow.value = host.clientWidth <= NARROW_HOST_WIDTH\n hostObserver = new ResizeObserver((entries) => {\n const width = entries[0]?.contentRect.width ?? host.clientWidth\n if (width > 0)\n isNarrow.value = width <= NARROW_HOST_WIDTH\n })\n hostObserver.observe(host)\n})\nonBeforeUnmount(() => hostObserver?.disconnect())\n\nconst step = ref(0)\nconst attempted = ref<ReadonlySet<number>>(new Set())\nconst isComplete = ref(false)\n\nconst quantities = ref<Record<string, number>>(Object.fromEntries(CART_ITEMS.map(item => [item.id, item.qty])))\nconst promoInput = ref('')\nconst promoWasChecked = ref(false)\nconst appliedPromo = ref<string | null>(null)\n\nconst email = ref('')\nconst firstName = ref('')\nconst lastName = ref('')\nconst address = ref('')\nconst apartment = ref('')\nconst city = ref('')\nconst state = ref('')\nconst postalCode = ref('')\nconst country = ref('us')\n\nconst delivery = ref('standard')\nconst deliveryNote = ref('')\nconst isGift = ref(false)\n\nconst cardNumber = ref('')\nconst cardName = ref('')\nconst expiry = ref('')\nconst cvc = ref('')\nconst billingMatches = ref(true)\n\nconst items = computed(() =>\n CART_ITEMS\n .map(item => ({ ...item, qty: quantities.value[item.id] ?? item.qty }))\n .filter(item => item.qty > 0),\n)\n\nconst subtotal = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0))\nconst promo = computed(() => (appliedPromo.value ? PROMOS[appliedPromo.value] : undefined))\nconst discount = computed(() => (promo.value ? subtotal.value * promo.value.rate : 0))\nconst deliveryOption = computed(() => DELIVERY_OPTIONS.find(o => o.value === delivery.value) ?? DELIVERY_OPTIONS[0])\nconst shipping = computed(() =>\n subtotal.value - discount.value >= FREE_SHIPPING_THRESHOLD && delivery.value === 'standard'\n ? 0\n : deliveryOption.value.price,\n)\nconst tax = computed(() => (subtotal.value - discount.value) * TAX_RATE)\nconst total = computed(() => subtotal.value - discount.value + shipping.value + tax.value)\nconst itemCount = computed(() => items.value.reduce((sum, item) => sum + item.qty, 0))\n\nconst postalRule = computed(() => POSTAL_CODE_RULES[country.value] ?? POSTAL_CODE_RULES.us)\n\nconst errorsByStep = computed<Array<Record<string, string>>>(() => {\n const cart: Record<string, string> = {}\n if (items.value.length === 0)\n cart.items = 'Your cart is empty.'\n\n const shippingStep: Record<string, string> = {}\n if (!EMAIL_PATTERN.test(email.value))\n shippingStep.email = 'Enter an email address for order updates.'\n if (!firstName.value.trim())\n shippingStep.firstName = 'Required'\n if (!lastName.value.trim())\n shippingStep.lastName = 'Required'\n if (!address.value.trim())\n shippingStep.address = 'Required'\n if (!city.value.trim())\n shippingStep.city = 'Required'\n if (country.value === 'us' && !state.value)\n shippingStep.state = 'Required'\n if (!postalRule.value.pattern.test(postalCode.value.trim()))\n shippingStep.postalCode = postalRule.value.error\n\n const deliveryStep: Record<string, string> = {}\n if (delivery.value === 'overnight' && country.value !== 'us')\n deliveryStep.delivery = 'Overnight delivery is only available in the US.'\n\n const payment: Record<string, string> = {}\n if (cardNumber.value.replace(/\\s/g, '').length < 15)\n payment.cardNumber = 'Enter the full card number.'\n if (!cardName.value.trim())\n payment.cardName = 'Enter the name printed on the card.'\n if (!/^\\d{2}\\s?\\/\\s?\\d{2}$/.test(expiry.value.trim()))\n payment.expiry = 'Use MM/YY.'\n if (!/^\\d{3,4}$/.test(cvc.value.trim()))\n payment.cvc = '3 or 4 digits.'\n\n return [cart, shippingStep, deliveryStep, payment]\n})\n\nfunction shownErrors(index: number): Record<string, string> {\n return attempted.value.has(index) ? errorsByStep.value[index] : {}\n}\n\nconst currentErrors = computed(() => shownErrors(step.value))\nconst isLastStep = computed(() => step.value === STEPS.length - 1)\n\nfunction markAttempted(indices: number[]) {\n const next = new Set(attempted.value)\n for (const index of indices) next.add(index)\n attempted.value = next\n}\n\nfunction goNext() {\n markAttempted([step.value])\n if (Object.keys(errorsByStep.value[step.value]).length > 0)\n return\n if (isLastStep.value) {\n isComplete.value = true\n return\n }\n step.value += 1\n}\n\n/** 往前跳时先检查中间的步骤,遇到第一个有问题的就停在那里。 */\nfunction goTo(index: number) {\n if (index <= step.value) {\n step.value = index\n return\n }\n const checked: number[] = []\n let firstInvalid: number | null = null\n for (let candidate = step.value; candidate < index; candidate += 1) {\n checked.push(candidate)\n if (Object.keys(errorsByStep.value[candidate]).length > 0) {\n firstInvalid = candidate\n break\n }\n }\n markAttempted(checked)\n step.value = firstInvalid ?? index\n}\n\nfunction onPromoInput(value: string) {\n promoInput.value = value\n promoWasChecked.value = false\n appliedPromo.value = null\n}\n\nfunction applyPromo() {\n const code = promoInput.value.trim().toUpperCase()\n promoWasChecked.value = code.length > 0\n appliedPromo.value = PROMOS[code] ? code : null\n}\n\nconst promoStatus = computed<{ type: 'success' | 'warning', message: string } | undefined>(() => {\n if (appliedPromo.value)\n return { type: 'success', message: `${PROMOS[appliedPromo.value].label} applied.` }\n if (promoInput.value.trim() && promoWasChecked.value)\n return { type: 'warning', message: 'That promo code is not valid.' }\n if (promoInput.value.trim())\n return { type: 'warning', message: 'Press Apply to check this code.' }\n return undefined\n})\n\nfunction blockedMessage(count: number) {\n return count === 1 ? 'One problem above needs fixing first.' : `${count} problems above need fixing first.`\n}\n\nfunction setQuantity(id: string, value: number | null) {\n quantities.value = { ...quantities.value, [id]: value ?? 0 }\n}\n\nfunction isFreeDelivery(optionValue: string) {\n return optionValue === 'standard' && subtotal.value - discount.value >= FREE_SHIPPING_THRESHOLD\n}\n<\/script>\n\n<template>\n <Layout height=\"fill\" :padding=\"6\" :content-width=\"1000\">\n <LayoutContent>\n <VStack ref=\"hostRef\" :gap=\"6\">\n <HStack :gap=\"3\" align=\"center\">\n <StackItem size=\"fill\">\n <Heading :level=\"1\">\n Checkout\n </Heading>\n </StackItem>\n <HStack :gap=\"1.5\" align=\"center\">\n <Icon name=\"heroicons:lock-closed\" size=\"sm\" color=\"secondary\" />\n <Text type=\"supporting\" color=\"secondary\">\n Secure checkout\n </Text>\n </HStack>\n </HStack>\n\n <Stack :direction=\"isNarrow ? 'vertical' : 'horizontal'\" :gap=\"6\" align=\"start\">\n <StackItem size=\"fill\" :cross-align-self=\"isNarrow ? 'stretch' : undefined\">\n <VStack :gap=\"6\">\n <Banner\n v-if=\"isComplete\"\n status=\"success\"\n title=\"Order confirmed\"\n :description=\"`This local example completed an order for ${money(total)}. No payment was sent.`\"\n />\n <VStack v-if=\"isComplete\" :gap=\"4\">\n <Heading :level=\"2\">\n Thanks for your order\n </Heading>\n <Text type=\"body\" color=\"secondary\">\n The completion is stored in this page's component state so the template ends in a real, reviewable outcome.\n </Text>\n </VStack>\n\n <Stepper\n v-if=\"!isComplete\"\n :active-step=\"step\"\n orientation=\"horizontal\"\n density=\"balanced\"\n label=\"Checkout progress\"\n @step-click=\"goTo\"\n >\n <Step\n v-for=\"({ label, icon }, i) in STEPS\"\n :key=\"label\"\n :step=\"i\"\n :label=\"label\"\n :status=\"Object.keys(shownErrors(i)).length ? 'error' : undefined\"\n >\n <template #icon>\n <Icon v-if=\"Object.keys(shownErrors(i)).length\" icon=\"error\" size=\"sm\" />\n <Icon v-else :name=\"icon\" size=\"sm\" />\n </template>\n </Step>\n </Stepper>\n\n <VStack v-if=\"!isComplete && step === 0\" :gap=\"5\">\n <Heading :level=\"2\">\n Review your cart\n </Heading>\n <VStack :gap=\"4\">\n <VStack v-for=\"item in items\" :key=\"item.id\" :gap=\"3\">\n <HStack :gap=\"4\" align=\"start\" wrap=\"wrap\">\n <Thumbnail :src=\"item.src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"1\">\n <HStack :gap=\"2\" align=\"start\" justify=\"between\">\n <HStack :gap=\"2\" align=\"center\" wrap=\"wrap\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Badge v-if=\"item.isLimited\" variant=\"green\" label=\"Limited\" />\n </HStack>\n <Text type=\"body\" weight=\"bold\">\n {{ money(item.price * item.qty) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }} · {{ money(item.price) }} each\n </Text>\n <HStack :gap=\"2\" align=\"center\">\n <span class=\"quantity-field\">\n <NumberInput\n :model-value=\"item.qty\"\n :label=\"`Quantity of ${item.name}`\"\n is-label-hidden\n :min=\"0\"\n :max=\"10\"\n is-integer-only\n has-number-steppers\n @update:model-value=\"setQuantity(item.id, $event)\"\n />\n </span>\n <Button label=\"Remove\" variant=\"ghost\" @click=\"setQuantity(item.id, 0)\" />\n </HStack>\n </VStack>\n </StackItem>\n </HStack>\n <Divider />\n </VStack>\n </VStack>\n <FieldStatus\n v-if=\"currentErrors.items\"\n type=\"error\"\n variant=\"detached\"\n :message=\"currentErrors.items\"\n />\n <HStack :gap=\"2\" align=\"end\">\n <StackItem size=\"fill\">\n <TextInput\n :model-value=\"promoInput\"\n label=\"Promo code\"\n placeholder=\"WELCOME10\"\n :status=\"promoStatus\"\n @update:model-value=\"onPromoInput($event)\"\n />\n </StackItem>\n <Button label=\"Apply\" variant=\"secondary\" @click=\"applyPromo\" />\n </HStack>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 1\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n Where should it go?\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n Delivery options and tax are calculated from this address.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <TextInput\n v-model=\"email\"\n label=\"Email\"\n placeholder=\"you@example.com\"\n description=\"Order confirmation and tracking go here.\"\n :status=\"currentErrors.email ? { type: 'error', message: currentErrors.email } : undefined\"\n />\n <FormLayout :direction=\"isNarrow ? 'vertical' : 'horizontal'\" default-optionality=\"required\">\n <TextInput\n v-model=\"firstName\"\n label=\"First name\"\n :status=\"currentErrors.firstName ? { type: 'error', message: currentErrors.firstName } : undefined\"\n />\n <TextInput\n v-model=\"lastName\"\n label=\"Last name\"\n :status=\"currentErrors.lastName ? { type: 'error', message: currentErrors.lastName } : undefined\"\n />\n </FormLayout>\n <Selector v-model=\"country\" label=\"Country\" :options=\"COUNTRIES\" />\n <TextInput\n v-model=\"address\"\n label=\"Street address\"\n placeholder=\"123 Main Street\"\n :status=\"currentErrors.address ? { type: 'error', message: currentErrors.address } : undefined\"\n />\n <TextInput v-model=\"apartment\" label=\"Apartment, suite, etc.\" is-optional />\n <FormLayout :direction=\"isNarrow ? 'vertical' : 'horizontal'\" default-optionality=\"required\">\n <TextInput\n v-model=\"city\"\n label=\"City\"\n :status=\"currentErrors.city ? { type: 'error', message: currentErrors.city } : undefined\"\n />\n <Selector\n v-if=\"country === 'us'\"\n v-model=\"state\"\n label=\"State\"\n placeholder=\"Select state\"\n :options=\"US_STATES\"\n searchable\n :status=\"currentErrors.state ? 'error' : undefined\"\n :status-message=\"currentErrors.state\"\n />\n <TextInput\n v-model=\"postalCode\"\n :label=\"postalRule.label\"\n :placeholder=\"postalRule.placeholder\"\n :status=\"currentErrors.postalCode ? { type: 'error', message: currentErrors.postalCode } : undefined\"\n />\n </FormLayout>\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 2\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n How fast do you need it?\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n Estimates run from the day the order ships, usually 1–3 business days after you place it.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <RadioList\n v-model=\"delivery\"\n label=\"Delivery method\"\n :status=\"currentErrors.delivery ? { type: 'error', message: currentErrors.delivery } : undefined\"\n >\n <RadioListItem\n v-for=\"option in DELIVERY_OPTIONS\"\n :key=\"option.value\"\n :value=\"option.value\"\n :label=\"option.label\"\n :description=\"option.window\"\n >\n <template #end>\n <Text type=\"body\" weight=\"medium\">\n {{ isFreeDelivery(option.value) ? 'Free' : money(option.price) }}\n </Text>\n </template>\n </RadioListItem>\n </RadioList>\n <Divider />\n <CheckboxInput v-model=\"isGift\" label=\"This is a gift\" />\n <TextArea\n v-model=\"deliveryNote\"\n :label=\"isGift ? 'Gift message' : 'Delivery instructions'\"\n is-optional\n :rows=\"3\"\n :max-length=\"240\"\n :placeholder=\"isGift ? 'Happy birthday — hope these get plenty of use.' : 'Leave with the neighbour at number 14.'\"\n :description=\"isGift ? 'Printed on a card. Prices are left off the packing slip.' : 'Passed to the carrier where they support it.'\"\n />\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete && step === 3\" :gap=\"5\">\n <VStack :gap=\"1\">\n <Heading :level=\"2\">\n Payment\n </Heading>\n <Text type=\"supporting\" color=\"secondary\">\n This template validates payment details and completes locally. No card is charged.\n </Text>\n </VStack>\n <FormLayout default-optionality=\"required\">\n <TextInput\n v-model=\"cardNumber\"\n label=\"Card number\"\n placeholder=\"4242 4242 4242 4242\"\n :status=\"currentErrors.cardNumber ? { type: 'error', message: currentErrors.cardNumber } : undefined\"\n />\n <TextInput\n v-model=\"cardName\"\n label=\"Name on card\"\n :status=\"currentErrors.cardName ? { type: 'error', message: currentErrors.cardName } : undefined\"\n />\n <HStack :gap=\"4\" wrap=\"wrap\">\n <TextInput\n v-model=\"expiry\"\n label=\"Expiry\"\n :width=\"140\"\n placeholder=\"MM/YY\"\n :status=\"currentErrors.expiry ? { type: 'error', message: currentErrors.expiry } : undefined\"\n />\n <TextInput\n v-model=\"cvc\"\n label=\"CVC\"\n :width=\"140\"\n placeholder=\"123\"\n label-tooltip=\"Three digits on the back of the card, or four on the front for American Express.\"\n :status=\"currentErrors.cvc ? { type: 'error', message: currentErrors.cvc } : undefined\"\n />\n </HStack>\n <Divider />\n <CheckboxInput v-model=\"billingMatches\" label=\"Billing address matches the shipping address\" />\n <Banner\n v-if=\"!billingMatches\"\n status=\"info\"\n title=\"Connect a billing-address step\"\n description=\"This local example does not collect a separate billing address or send payment. Add that flow with your payment provider.\"\n />\n </FormLayout>\n </VStack>\n\n <VStack v-if=\"!isComplete\" :gap=\"3\" h-align=\"start\">\n <FieldStatus\n v-if=\"Object.keys(currentErrors).length > 0\"\n type=\"error\"\n variant=\"detached\"\n :message=\"blockedMessage(Object.keys(currentErrors).length)\"\n />\n <HStack :gap=\"3\" align=\"center\" width=\"100%\">\n <Button\n :label=\"step === 0 ? 'Continue shopping' : 'Back'\"\n variant=\"secondary\"\n @click=\"step = Math.max(0, step - 1)\"\n />\n <StackItem size=\"fill\">\n <Button\n :label=\"isLastStep ? `Pay ${money(total)}` : 'Continue'\"\n variant=\"primary\"\n width=\"100%\"\n @click=\"goNext\"\n />\n </StackItem>\n </HStack>\n </VStack>\n </VStack>\n </StackItem>\n\n <StackItem :class=\"isNarrow ? 'summary-column summary-column--narrow' : 'summary-column'\">\n <Card :padding=\"5\">\n <component\n :is=\"isNarrow ? Collapsible : VStack\"\n v-bind=\"isNarrow ? { trigger: `Order summary · ${money(total)}`, defaultIsOpen: false } : { gap: 4 }\"\n >\n <HStack v-if=\"!isNarrow\" :gap=\"2\" align=\"center\" justify=\"between\" class=\"summary-title\">\n <Text type=\"label\">\n Order summary\n </Text>\n <Badge variant=\"neutral\" :label=\"`${itemCount} ${itemCount === 1 ? 'item' : 'items'}`\" />\n </HStack>\n <VStack :gap=\"4\" :padding-block-start=\"isNarrow ? 3 : undefined\">\n <VStack :gap=\"3\">\n <HStack v-for=\"item in items\" :key=\"item.id\" :gap=\"3\" align=\"start\">\n <Thumbnail :src=\"item.src\" :alt=\"item.name\" />\n <StackItem size=\"fill\">\n <VStack :gap=\"0.5\">\n <HStack :gap=\"2\" align=\"start\" justify=\"between\">\n <Text type=\"body\" weight=\"medium\">\n {{ item.name }}\n </Text>\n <Text type=\"body\" weight=\"bold\">\n {{ money(item.price * item.qty) }}\n </Text>\n </HStack>\n <Text type=\"supporting\" color=\"secondary\">\n {{ item.variant }} · Qty {{ item.qty }}\n </Text>\n </VStack>\n </StackItem>\n </HStack>\n </VStack>\n <Divider />\n <VStack :gap=\"2\">\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Subtotal\n </Text>\n <Text type=\"body\">\n {{ money(subtotal) }}\n </Text>\n </HStack>\n <HStack v-if=\"promo\" justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n {{ promo.label }}\n </Text>\n <Text type=\"body\">\n −{{ money(discount) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n {{ deliveryOption.label }} delivery\n </Text>\n <Text type=\"body\">\n {{ shipping === 0 ? 'Free' : money(shipping) }}\n </Text>\n </HStack>\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"body\" color=\"secondary\">\n Estimated tax\n </Text>\n <Text type=\"body\">\n {{ money(tax) }}\n </Text>\n </HStack>\n <Divider />\n <HStack justify=\"between\" align=\"center\">\n <Text type=\"large\" weight=\"bold\">\n Total\n </Text>\n <Text type=\"large\" weight=\"bold\">\n {{ money(total) }}\n </Text>\n </HStack>\n </VStack>\n <Banner\n v-if=\"subtotal - discount < FREE_SHIPPING_THRESHOLD\"\n status=\"info\"\n :title=\"`${money(FREE_SHIPPING_THRESHOLD - (subtotal - discount))} away from free standard shipping`\"\n />\n </VStack>\n </component>\n </Card>\n </StackItem>\n </Stack>\n </VStack>\n </LayoutContent>\n </Layout>\n</template>\n\n<style scoped>\n/* 摘要列固定 360 宽,表单滚过时贴在页面内边距处;窄版时排到表单前面、占满一行。 */\n.summary-column {\n position: sticky;\n top: var(--astryx-spacing-6);\n flex-shrink: 0;\n align-self: flex-start;\n width: 360px;\n}\n.summary-column--narrow {\n position: static;\n order: -1;\n align-self: stretch;\n width: auto;\n}\n.quantity-field {\n display: block;\n flex: none;\n width: 96px;\n}\n</style>\n",
|
|
77913
79725
|
"docsUrl": {
|
|
77914
79726
|
"en": "https://astryx-vue.pages.dev/en/playground/?template=checkout-wizard",
|
|
77915
79727
|
"zh": "https://astryx-vue.pages.dev/playground/?template=checkout-wizard"
|
|
@@ -78053,7 +79865,7 @@ const catalog = {
|
|
|
78053
79865
|
],
|
|
78054
79866
|
"dependencies": ["@astryx-vue/core", "vue"],
|
|
78055
79867
|
"isReady": true,
|
|
78056
|
-
"source": "<script setup lang=\"ts\">\nimport type { ISOTimeString } from '@astryx-vue/core'\nimport { computed, ref, useId } from 'vue'\nimport {\n Banner,\n Button,\n Center,\n CheckboxList,\n CheckboxListItem,\n DateInput,\n Dialog,\n DialogHeader,\n Divider,\n Field,\n FieldStatus,\n FormLayout,\n HStack,\n Layout,\n LayoutContent,\n LayoutFooter,\n LayoutHeader,\n MetadataList,\n MetadataListItem,\n MultiSelector,\n RadioList,\n RadioListItem,\n SegmentedControl,\n SegmentedControlItem,\n Selector,\n StackItem,\n Step,\n Stepper,\n Switch,\n TextArea,\n TextInput,\n TimeInput,\n useToast,\n VStack,\n} from '@astryx-vue/core'\n\nconst STEPS = ['Report', 'Schedule', 'Delivery']\n\nconst DATASETS = [\n { value: 'pipeline', label: 'Sales pipeline' },\n { value: 'revenue', label: 'Revenue and bookings' },\n { value: 'churn', label: 'Churn and retention' },\n { value: 'support', label: 'Support tickets' },\n { value: 'usage', label: 'Product usage' },\n]\n\nconst SECTIONS = [\n { value: 'summary', label: 'Executive summary', description: 'Headline numbers and week-over-week change.' },\n { value: 'charts', label: 'Trend charts', description: 'Twelve-week series for each headline metric.' },\n { value: 'breakdown', label: 'Segment breakdown', description: 'Split by region, plan, and account size.' },\n { value: 'anomalies', label: 'Anomaly callouts', description: 'Metrics more than two standard deviations off trend.' },\n]\n\nconst RECIPIENTS = [\n { value: 'exec', label: 'exec-staff@northwind.dev' },\n { value: 'sales', label: 'Sales leadership (9 people)' },\n { value: 'finance', label: 'finance@northwind.dev' },\n { value: 'amara', label: 'Amara Diallo' },\n { value: 'felix', label: 'Felix Tran' },\n]\n\nconst TIMEZONES = [\n { value: 'pt', label: 'Pacific Time (PT)' },\n { value: 'et', label: 'Eastern Time (ET)' },\n { value: 'gmt', label: 'Greenwich Mean Time (GMT)' },\n { value: 'cet', label: 'Central European Time (CET)' },\n]\n\nconst WEEKDAYS = [\n { value: 'mon', label: 'Monday' },\n { value: 'tue', label: 'Tuesday' },\n { value: 'wed', label: 'Wednesday' },\n { value: 'thu', label: 'Thursday' },\n { value: 'fri', label: 'Friday' },\n]\n\nfunction blockedMessage(count: number) {\n return count === 1\n ? 'One problem above needs fixing first.'\n : `${count} problems above need fixing first.`\n}\n\nconst showToast = useToast()\nconst cadenceInputID = useId()\nconst cadenceLabelID = useId()\n\nconst step = ref(0)\nconst attempted = ref<ReadonlySet<number>>(new Set())\n\nconst name = ref('')\nconst dataset = ref('pipeline')\nconst sections = ref<string[]>(['summary', 'charts'])\nconst note = ref('')\n\nconst cadence = ref('weekly')\nconst weekday = ref('mon')\nconst time = ref<ISOTimeString | undefined>('08:00' as ISOTimeString)\nconst timezone = ref('pt')\nconst startDate = ref<Date | null>(null)\n\nconst recipients = ref<string[]>(['exec'])\nconst format = ref('pdf')\nconst skipEmpty = ref(true)\n\nfunction resetDraft() {\n step.value = 0\n attempted.value = new Set()\n name.value = ''\n dataset.value = 'pipeline'\n sections.value = ['summary', 'charts']\n note.value = ''\n cadence.value = 'weekly'\n weekday.value = 'mon'\n time.value = '08:00' as ISOTimeString\n timezone.value = 'pt'\n startDate.value = null\n recipients.value = ['exec']\n format.value = 'pdf'\n skipEmpty.value = true\n}\n\nfunction handleOpenChange(isOpen: boolean) {\n if (!isOpen)\n resetDraft()\n}\n\ntype StepErrors = Partial<Record<'name' | 'sections' | 'time' | 'startDate' | 'recipients', string>>\n\n/** 每个步骤的必填项检查,顺序和 STEPS 一致。 */\nfunction collectErrors(): StepErrors[] {\n return [\n {\n ...(name.value.trim() ? {} : { name: 'Give the report a name.' }),\n ...(sections.value.length === 0 ? { sections: 'Include at least one section.' } : {}),\n },\n {\n ...(time.value ? {} : { time: 'Pick a send time.' }),\n ...(startDate.value ? {} : { startDate: 'Pick a start date.' }),\n },\n {\n ...(recipients.value.length === 0 ? { recipients: 'Add at least one recipient.' } : {}),\n },\n ]\n}\n\nfunction errorsForStep(index: number): StepErrors {\n return attempted.value.has(index) ? collectErrors()[index]! : {}\n}\n\nconst currentErrors = computed(() => errorsForStep(step.value))\nconst isLastStep = computed(() => step.value === STEPS.length - 1)\n\nconst cadenceSummary = computed(() => {\n if (cadence.value === 'daily')\n return `Every weekday at ${time.value ?? '—'}`\n if (cadence.value === 'weekly')\n return `${WEEKDAYS.find(d => d.value === weekday.value)?.label ?? weekday.value}s at ${time.value ?? '—'}`\n return `1st of the month at ${time.value ?? '—'}`\n})\n\nfunction goNext() {\n attempted.value = new Set(attempted.value).add(step.value)\n const errs = collectErrors()\n if (Object.keys(errs[step.value]!).length !== 0)\n return\n if (!isLastStep.value) {\n step.value += 1\n return\n }\n showToast({\n body: `${name.value.trim()} is scheduled · ${cadenceSummary.value}`,\n uniqueID: 'report-scheduled',\n })\n resetDraft()\n}\n<\/script>\n\n<template>\n <Center height=\"100dvh\">\n <Dialog\n is-open\n is-inline\n :width=\"620\"\n purpose=\"form\"\n @update:open=\"handleOpenChange\"\n >\n <Layout height=\"fill\" :padding=\"4\" default-has-dividers>\n <template #header>\n <DialogHeader title=\"Schedule a report\" @update:open=\"handleOpenChange\" :has-divider=\"false\" />\n <LayoutHeader :has-divider=\"false\">\n <Stepper\n :active-step=\"step\"\n orientation=\"horizontal\"\n density=\"compact\"\n label=\"Report setup progress\"\n >\n <Step\n v-for=\"(label, i) in STEPS\"\n :key=\"label\"\n :step=\"i\"\n :label=\"label\"\n :status=\"Object.keys(errorsForStep(i)).length > 0 ? 'error' : undefined\"\n />\n </Stepper>\n </LayoutHeader>\n </template>\n <template #default>\n <LayoutContent>\n <VStack :gap=\"5\">\n <FormLayout v-if=\"step === 0\" default-optionality=\"optional\">\n <TextInput\n v-model=\"name\"\n label=\"Report name\"\n is-required\n placeholder=\"Weekly pipeline review\"\n description=\"Used as the email subject line.\"\n :status=\"currentErrors.name ? { type: 'error', message: currentErrors.name } : undefined\"\n />\n <Selector v-model=\"dataset\" label=\"Dataset\" :options=\"DATASETS\" />\n <CheckboxList\n v-model=\"sections\"\n label=\"Sections to include\"\n density=\"compact\"\n :status=\"currentErrors.sections ? { type: 'error', message: currentErrors.sections } : undefined\"\n >\n <CheckboxListItem\n v-for=\"section in SECTIONS\"\n :key=\"section.value\"\n :value=\"section.value\"\n :label=\"section.label\"\n :description=\"section.description\"\n />\n </CheckboxList>\n </FormLayout>\n\n <FormLayout v-else-if=\"step === 1\" default-optionality=\"optional\">\n <Field\n label=\"How often\"\n :input-id=\"cadenceInputID\"\n :label-id=\"cadenceLabelID\"\n is-group-label\n >\n <SegmentedControl\n v-model=\"cadence\"\n label=\"How often\"\n :aria-labelledby=\"cadenceLabelID\"\n layout=\"fill\"\n >\n <SegmentedControlItem value=\"daily\" label=\"Weekdays\" />\n <SegmentedControlItem value=\"weekly\" label=\"Weekly\" />\n <SegmentedControlItem value=\"monthly\" label=\"Monthly\" />\n </SegmentedControl>\n </Field>\n <Selector\n v-if=\"cadence === 'weekly'\"\n v-model=\"weekday\"\n label=\"Day of the week\"\n :options=\"WEEKDAYS\"\n />\n <FormLayout direction=\"horizontal\" default-optionality=\"optional\">\n <TimeInput\n v-model=\"time\"\n label=\"Send at\"\n is-required\n :status=\"currentErrors.time ? { type: 'error', message: currentErrors.time } : undefined\"\n />\n <Selector v-model=\"timezone\" label=\"Time zone\" :options=\"TIMEZONES\" />\n </FormLayout>\n <DateInput\n v-model=\"startDate\"\n label=\"First run\"\n is-required\n :status=\"currentErrors.startDate ? { type: 'error', message: currentErrors.startDate } : undefined\"\n />\n <Banner\n status=\"info\"\n :title=\"cadenceSummary\"\n description=\"Data is cut at the send time, so a report at 08:00 covers everything up to 08:00.\"\n />\n </FormLayout>\n\n <FormLayout v-else default-optionality=\"optional\">\n <MultiSelector\n v-model=\"recipients\"\n label=\"Recipients\"\n is-required\n :options=\"RECIPIENTS\"\n has-search\n trigger-display=\"badges\"\n description=\"Groups resolve at send time, so new members are included automatically.\"\n :status=\"currentErrors.recipients ? 'error' : 'default'\"\n :status-message=\"currentErrors.recipients\"\n />\n <RadioList v-model=\"format\" label=\"Attachment format\" size=\"sm\">\n <RadioListItem value=\"pdf\" label=\"PDF\" description=\"Best for reading. Charts render as images.\" />\n <RadioListItem value=\"csv\" label=\"CSV\" description=\"Raw rows, no charts or summary.\" />\n <RadioListItem value=\"both\" label=\"Both\" description=\"PDF in the body, CSV attached.\" />\n </RadioList>\n <Switch\n v-model=\"skipEmpty\"\n label=\"Skip the send when there is no new data\"\n label-position=\"start\"\n label-spacing=\"spread\"\n />\n <TextArea\n v-model=\"note\"\n label=\"Note in the email\"\n :rows=\"2\"\n :max-length=\"200\"\n placeholder=\"Numbers are provisional until finance closes the month.\"\n />\n <Divider />\n <MetadataList columns=\"single\">\n <MetadataListItem label=\"Report\">\n {{ name.trim() || 'Untitled' }}\n </MetadataListItem>\n <MetadataListItem label=\"Runs\">\n {{ cadenceSummary }}\n {{ TIMEZONES.find(t => t.value === timezone)?.label.match(/\\(([^)]+)\\)/)?.[1] ?? '' }}\n </MetadataListItem>\n <MetadataListItem label=\"Sent to\">\n {{ recipients.length === 0 ? '—' : `${recipients.length} ${recipients.length === 1 ? 'recipient' : 'recipients'}` }}\n </MetadataListItem>\n </MetadataList>\n </FormLayout>\n </VStack>\n </LayoutContent>\n </template>\n <template #footer>\n <LayoutFooter>\n <HStack :gap=\"2\" align=\"center\">\n <Button\n label=\"Back\"\n variant=\"secondary\"\n :is-disabled=\"step === 0\"\n @click=\"step = Math.max(0, step - 1)\"\n />\n <StackItem size=\"fill\" />\n <FieldStatus\n v-if=\"Object.keys(currentErrors).length > 0\"\n type=\"error\"\n variant=\"detached\"\n :message=\"blockedMessage(Object.keys(currentErrors).length)\"\n />\n <Button\n :label=\"isLastStep ? 'Schedule report' : 'Continue'\"\n variant=\"primary\"\n @click=\"goNext\"\n />\n </HStack>\n </LayoutFooter>\n </template>\n </Layout>\n </Dialog>\n </Center>\n</template>\n",
|
|
79868
|
+
"source": "<script setup lang=\"ts\">\nimport type { ISOTimeString } from '@astryx-vue/core'\nimport { computed, ref, useId } from 'vue'\nimport {\n Banner,\n Button,\n Center,\n CheckboxList,\n CheckboxListItem,\n DateInput,\n Dialog,\n DialogHeader,\n Divider,\n Field,\n FieldStatus,\n FormLayout,\n HStack,\n Layout,\n LayoutContent,\n LayoutFooter,\n LayoutHeader,\n MetadataList,\n MetadataListItem,\n MultiSelector,\n RadioList,\n RadioListItem,\n SegmentedControl,\n SegmentedControlItem,\n Selector,\n StackItem,\n Step,\n Stepper,\n Switch,\n TextArea,\n TextInput,\n TimeInput,\n useToast,\n VStack,\n} from '@astryx-vue/core'\n\nconst STEPS = ['Report', 'Schedule', 'Delivery']\n\nconst DATASETS = [\n { value: 'pipeline', label: 'Sales pipeline' },\n { value: 'revenue', label: 'Revenue and bookings' },\n { value: 'churn', label: 'Churn and retention' },\n { value: 'support', label: 'Support tickets' },\n { value: 'usage', label: 'Product usage' },\n]\n\nconst SECTIONS = [\n { value: 'summary', label: 'Executive summary', description: 'Headline numbers and week-over-week change.' },\n { value: 'charts', label: 'Trend charts', description: 'Twelve-week series for each headline metric.' },\n { value: 'breakdown', label: 'Segment breakdown', description: 'Split by region, plan, and account size.' },\n { value: 'anomalies', label: 'Anomaly callouts', description: 'Metrics more than two standard deviations off trend.' },\n]\n\nconst RECIPIENTS = [\n { value: 'exec', label: 'exec-staff@northwind.dev' },\n { value: 'sales', label: 'Sales leadership (9 people)' },\n { value: 'finance', label: 'finance@northwind.dev' },\n { value: 'amara', label: 'Amara Diallo' },\n { value: 'felix', label: 'Felix Tran' },\n]\n\nconst TIMEZONES = [\n { value: 'pt', label: 'Pacific Time (PT)' },\n { value: 'et', label: 'Eastern Time (ET)' },\n { value: 'gmt', label: 'Greenwich Mean Time (GMT)' },\n { value: 'cet', label: 'Central European Time (CET)' },\n]\n\nconst WEEKDAYS = [\n { value: 'mon', label: 'Monday' },\n { value: 'tue', label: 'Tuesday' },\n { value: 'wed', label: 'Wednesday' },\n { value: 'thu', label: 'Thursday' },\n { value: 'fri', label: 'Friday' },\n]\n\nfunction blockedMessage(count: number) {\n return count === 1\n ? 'One problem above needs fixing first.'\n : `${count} problems above need fixing first.`\n}\n\nconst showToast = useToast()\nconst cadenceInputID = useId()\nconst cadenceLabelID = useId()\n\nconst step = ref(0)\nconst attempted = ref<ReadonlySet<number>>(new Set())\n\nconst name = ref('')\nconst dataset = ref('pipeline')\nconst sections = ref<string[]>(['summary', 'charts'])\nconst note = ref('')\n\nconst cadence = ref('weekly')\nconst weekday = ref('mon')\nconst time = ref<ISOTimeString | undefined>('08:00' as ISOTimeString)\nconst timezone = ref('pt')\nconst startDate = ref<Date | null>(null)\n\nconst recipients = ref<string[]>(['exec'])\nconst format = ref('pdf')\nconst skipEmpty = ref(true)\n\nfunction resetDraft() {\n step.value = 0\n attempted.value = new Set()\n name.value = ''\n dataset.value = 'pipeline'\n sections.value = ['summary', 'charts']\n note.value = ''\n cadence.value = 'weekly'\n weekday.value = 'mon'\n time.value = '08:00' as ISOTimeString\n timezone.value = 'pt'\n startDate.value = null\n recipients.value = ['exec']\n format.value = 'pdf'\n skipEmpty.value = true\n}\n\nfunction handleOpenChange(isOpen: boolean) {\n if (!isOpen)\n resetDraft()\n}\n\ntype StepErrors = Partial<Record<'name' | 'sections' | 'time' | 'startDate' | 'recipients', string>>\n\n/** 每个步骤的必填项检查,顺序和 STEPS 一致。 */\nfunction collectErrors(): StepErrors[] {\n return [\n {\n ...(name.value.trim() ? {} : { name: 'Give the report a name.' }),\n ...(sections.value.length === 0 ? { sections: 'Include at least one section.' } : {}),\n },\n {\n ...(time.value ? {} : { time: 'Pick a send time.' }),\n ...(startDate.value ? {} : { startDate: 'Pick a start date.' }),\n },\n {\n ...(recipients.value.length === 0 ? { recipients: 'Add at least one recipient.' } : {}),\n },\n ]\n}\n\nfunction errorsForStep(index: number): StepErrors {\n return attempted.value.has(index) ? collectErrors()[index]! : {}\n}\n\nconst currentErrors = computed(() => errorsForStep(step.value))\nconst isLastStep = computed(() => step.value === STEPS.length - 1)\n\nconst cadenceSummary = computed(() => {\n if (cadence.value === 'daily')\n return `Every weekday at ${time.value ?? '—'}`\n if (cadence.value === 'weekly')\n return `${WEEKDAYS.find(d => d.value === weekday.value)?.label ?? weekday.value}s at ${time.value ?? '—'}`\n return `1st of the month at ${time.value ?? '—'}`\n})\n\nfunction goNext() {\n attempted.value = new Set(attempted.value).add(step.value)\n const errs = collectErrors()\n if (Object.keys(errs[step.value]!).length !== 0)\n return\n if (!isLastStep.value) {\n step.value += 1\n return\n }\n showToast({\n body: `${name.value.trim()} is scheduled · ${cadenceSummary.value}`,\n uniqueID: 'report-scheduled',\n })\n resetDraft()\n}\n<\/script>\n\n<template>\n <Center height=\"100dvh\">\n <Dialog\n is-open\n is-inline\n :width=\"620\"\n class=\"form-wizard-dialog\"\n purpose=\"form\"\n @update:open=\"handleOpenChange\"\n >\n <Layout height=\"fill\" :padding=\"4\" default-has-dividers>\n <template #header>\n <DialogHeader title=\"Schedule a report\" @update:open=\"handleOpenChange\" :has-divider=\"false\" />\n <LayoutHeader :has-divider=\"false\">\n <Stepper\n :active-step=\"step\"\n orientation=\"horizontal\"\n density=\"compact\"\n label=\"Report setup progress\"\n >\n <Step\n v-for=\"(label, i) in STEPS\"\n :key=\"label\"\n :step=\"i\"\n :label=\"label\"\n :status=\"Object.keys(errorsForStep(i)).length > 0 ? 'error' : undefined\"\n />\n </Stepper>\n </LayoutHeader>\n </template>\n <template #default>\n <LayoutContent :padding=\"4\">\n <VStack :gap=\"5\">\n <FormLayout v-if=\"step === 0\" default-optionality=\"optional\">\n <TextInput\n v-model=\"name\"\n label=\"Report name\"\n is-required\n placeholder=\"Weekly pipeline review\"\n description=\"Used as the email subject line.\"\n :status=\"currentErrors.name ? { type: 'error', message: currentErrors.name } : undefined\"\n />\n <Selector v-model=\"dataset\" label=\"Dataset\" :options=\"DATASETS\" />\n <CheckboxList\n v-model=\"sections\"\n label=\"Sections to include\"\n density=\"compact\"\n :status=\"currentErrors.sections ? { type: 'error', message: currentErrors.sections } : undefined\"\n >\n <CheckboxListItem\n v-for=\"section in SECTIONS\"\n :key=\"section.value\"\n :value=\"section.value\"\n :label=\"section.label\"\n :description=\"section.description\"\n />\n </CheckboxList>\n </FormLayout>\n\n <FormLayout v-else-if=\"step === 1\" default-optionality=\"optional\">\n <Field\n label=\"How often\"\n :input-id=\"cadenceInputID\"\n :label-id=\"cadenceLabelID\"\n is-group-label\n >\n <SegmentedControl\n v-model=\"cadence\"\n label=\"How often\"\n :aria-labelledby=\"cadenceLabelID\"\n layout=\"fill\"\n >\n <SegmentedControlItem value=\"daily\" label=\"Weekdays\" />\n <SegmentedControlItem value=\"weekly\" label=\"Weekly\" />\n <SegmentedControlItem value=\"monthly\" label=\"Monthly\" />\n </SegmentedControl>\n </Field>\n <Selector\n v-if=\"cadence === 'weekly'\"\n v-model=\"weekday\"\n label=\"Day of the week\"\n :options=\"WEEKDAYS\"\n />\n <FormLayout direction=\"horizontal\" default-optionality=\"optional\">\n <TimeInput\n v-model=\"time\"\n label=\"Send at\"\n is-required\n :status=\"currentErrors.time ? { type: 'error', message: currentErrors.time } : undefined\"\n />\n <Selector v-model=\"timezone\" label=\"Time zone\" :options=\"TIMEZONES\" />\n </FormLayout>\n <DateInput\n v-model=\"startDate\"\n label=\"First run\"\n is-required\n :status=\"currentErrors.startDate ? { type: 'error', message: currentErrors.startDate } : undefined\"\n />\n <Banner\n status=\"info\"\n :title=\"cadenceSummary\"\n description=\"Data is cut at the send time, so a report at 08:00 covers everything up to 08:00.\"\n />\n </FormLayout>\n\n <FormLayout v-else default-optionality=\"optional\">\n <MultiSelector\n v-model=\"recipients\"\n label=\"Recipients\"\n is-required\n :options=\"RECIPIENTS\"\n has-search\n trigger-display=\"badges\"\n description=\"Groups resolve at send time, so new members are included automatically.\"\n :status=\"currentErrors.recipients ? 'error' : 'default'\"\n :status-message=\"currentErrors.recipients\"\n />\n <RadioList v-model=\"format\" label=\"Attachment format\" size=\"sm\">\n <RadioListItem value=\"pdf\" label=\"PDF\" description=\"Best for reading. Charts render as images.\" />\n <RadioListItem value=\"csv\" label=\"CSV\" description=\"Raw rows, no charts or summary.\" />\n <RadioListItem value=\"both\" label=\"Both\" description=\"PDF in the body, CSV attached.\" />\n </RadioList>\n <Switch\n v-model=\"skipEmpty\"\n label=\"Skip the send when there is no new data\"\n label-position=\"start\"\n label-spacing=\"spread\"\n />\n <TextArea\n v-model=\"note\"\n label=\"Note in the email\"\n :rows=\"2\"\n :max-length=\"200\"\n placeholder=\"Numbers are provisional until finance closes the month.\"\n />\n <Divider />\n <MetadataList columns=\"single\">\n <MetadataListItem label=\"Report\">\n {{ name.trim() || 'Untitled' }}\n </MetadataListItem>\n <MetadataListItem label=\"Runs\">\n {{ cadenceSummary }}\n {{ TIMEZONES.find(t => t.value === timezone)?.label.match(/\\(([^)]+)\\)/)?.[1] ?? '' }}\n </MetadataListItem>\n <MetadataListItem label=\"Sent to\">\n {{ recipients.length === 0 ? '—' : `${recipients.length} ${recipients.length === 1 ? 'recipient' : 'recipients'}` }}\n </MetadataListItem>\n </MetadataList>\n </FormLayout>\n </VStack>\n </LayoutContent>\n </template>\n <template #footer>\n <LayoutFooter>\n <HStack :gap=\"2\" align=\"center\">\n <Button\n label=\"Back\"\n variant=\"secondary\"\n :is-disabled=\"step === 0\"\n @click=\"step = Math.max(0, step - 1)\"\n />\n <StackItem size=\"fill\" />\n <FieldStatus\n v-if=\"Object.keys(currentErrors).length > 0\"\n type=\"error\"\n variant=\"detached\"\n :message=\"blockedMessage(Object.keys(currentErrors).length)\"\n />\n <Button\n :label=\"isLastStep ? 'Schedule report' : 'Continue'\"\n variant=\"primary\"\n @click=\"goNext\"\n />\n </HStack>\n </LayoutFooter>\n </template>\n </Layout>\n </Dialog>\n </Center>\n</template>\n\n<style>\n.form-wizard-dialog .astryx-dialog__inner {\n height: auto;\n max-height: 75vh;\n}\n\n.form-wizard-dialog .astryx-layout,\n.form-wizard-dialog .astryx-layout__inner {\n height: auto;\n min-height: 0;\n max-height: calc(75vh - 32px);\n}\n</style>\n",
|
|
78057
79869
|
"docsUrl": {
|
|
78058
79870
|
"en": "https://astryx-vue.pages.dev/en/playground/?template=form-wizard-dialog",
|
|
78059
79871
|
"zh": "https://astryx-vue.pages.dev/playground/?template=form-wizard-dialog"
|
|
@@ -80924,7 +82736,7 @@ function wantsJson(argv) {
|
|
|
80924
82736
|
}
|
|
80925
82737
|
//#endregion
|
|
80926
82738
|
//#region package.json
|
|
80927
|
-
var version = "0.4.
|
|
82739
|
+
var version = "0.4.3";
|
|
80928
82740
|
//#endregion
|
|
80929
82741
|
//#region src/envelope.ts
|
|
80930
82742
|
function successEnvelope(type, data, packageName) {
|