@astryx-vue/cli 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2109 -363
  2. package/package.json +1 -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.5.0",
636
+ "version": "0.5.2",
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.0.0",
645
- "description": "Astryx charts for Vue — a config-model data visualization layer (d3-based). Ships only under the canary dist-tag.",
646
- "canary": true,
647
- "published": false
644
+ "version": "0.1.0",
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.0.0",
652
+ "version": "0.1.1",
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": false
655
+ "published": true
656
656
  },
657
657
  {
658
658
  "name": "@astryx-vue/richtext",
659
659
  "directory": "packages/richtext",
660
- "version": "0.5.0",
660
+ "version": "0.5.2",
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.5.0",
668
+ "version": "0.5.2",
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",
@@ -74735,217 +76831,6 @@ const catalog = {
74735
76831
  "zh": "https://astryx-vue.pages.dev/guide/changelog"
74736
76832
  }
74737
76833
  },
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
76834
  {
74950
76835
  "id": "guide/cli",
74951
76836
  "kind": "guide",
@@ -75667,145 +77552,6 @@ const catalog = {
75667
77552
  "zh": "https://astryx-vue.pages.dev/guide/responsive"
75668
77553
  }
75669
77554
  },
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
77555
  {
75810
77556
  "id": "guide/styling",
75811
77557
  "kind": "guide",
@@ -77835,7 +79581,7 @@ const catalog = {
77835
79581
  ],
77836
79582
  "dependencies": ["@astryx-vue/core", "vue"],
77837
79583
  "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",
79584
+ "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
79585
  "docsUrl": {
77840
79586
  "en": "https://astryx-vue.pages.dev/en/playground/?template=payment-form",
77841
79587
  "zh": "https://astryx-vue.pages.dev/playground/?template=payment-form"
@@ -77909,7 +79655,7 @@ const catalog = {
77909
79655
  ],
77910
79656
  "dependencies": ["@astryx-vue/core", "vue"],
77911
79657
  "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",
79658
+ "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
79659
  "docsUrl": {
77914
79660
  "en": "https://astryx-vue.pages.dev/en/playground/?template=checkout-wizard",
77915
79661
  "zh": "https://astryx-vue.pages.dev/playground/?template=checkout-wizard"
@@ -78053,7 +79799,7 @@ const catalog = {
78053
79799
  ],
78054
79800
  "dependencies": ["@astryx-vue/core", "vue"],
78055
79801
  "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",
79802
+ "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
79803
  "docsUrl": {
78058
79804
  "en": "https://astryx-vue.pages.dev/en/playground/?template=form-wizard-dialog",
78059
79805
  "zh": "https://astryx-vue.pages.dev/playground/?template=form-wizard-dialog"
@@ -80924,7 +82670,7 @@ function wantsJson(argv) {
80924
82670
  }
80925
82671
  //#endregion
80926
82672
  //#region package.json
80927
- var version = "0.4.1";
82673
+ var version = "0.4.2";
80928
82674
  //#endregion
80929
82675
  //#region src/envelope.ts
80930
82676
  function successEnvelope(type, data, packageName) {