@contentrain/astro-kit 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +16 -0
  2. package/catalog.json +76 -13
  3. package/components/_shared/Section.astro +13 -9
  4. package/components/_shared/SectionHeader.astro +3 -2
  5. package/components/_shared/instance-id.ts +24 -0
  6. package/components/_shared/muted.ts +19 -0
  7. package/components/band/Band.astro +10 -7
  8. package/components/band/meta.json +2 -0
  9. package/components/behavior/Behavior.astro +7 -2
  10. package/components/behavior/fixtures.json +2 -2
  11. package/components/behavior/meta.json +7 -0
  12. package/components/card-grid/CardGrid.astro +13 -9
  13. package/components/card-grid/meta.json +7 -0
  14. package/components/contact-details/ContactDetails.astro +7 -3
  15. package/components/contact-details/meta.json +2 -0
  16. package/components/contact-form/ContactForm.astro +11 -7
  17. package/components/contact-form/meta.json +2 -0
  18. package/components/cta/Cta.astro +14 -10
  19. package/components/cta/meta.json +2 -2
  20. package/components/dialog/Dialog.astro +6 -3
  21. package/components/dialog/meta.json +2 -0
  22. package/components/faq/Faq.astro +8 -4
  23. package/components/faq/meta.json +1 -1
  24. package/components/feature-list/FeatureList.astro +10 -6
  25. package/components/feature-list/meta.json +3 -1
  26. package/components/footer/Footer.astro +4 -3
  27. package/components/footer/meta.json +1 -0
  28. package/components/gallery/Gallery.astro +8 -4
  29. package/components/gallery/meta.json +2 -0
  30. package/components/header/Header.astro +8 -4
  31. package/components/header/meta.json +1 -1
  32. package/components/hero/Hero.astro +13 -9
  33. package/components/hero/meta.json +3 -1
  34. package/components/language-switcher/LanguageSwitcher.astro +3 -2
  35. package/components/language-switcher/meta.json +1 -1
  36. package/components/logo-cloud/LogoCloud.astro +9 -5
  37. package/components/logo-cloud/meta.json +3 -1
  38. package/components/nav/Nav.astro +10 -6
  39. package/components/nav/meta.json +2 -0
  40. package/components/newsletter/Newsletter.astro +9 -5
  41. package/components/newsletter/meta.json +2 -0
  42. package/components/post-list/PostList.astro +7 -3
  43. package/components/post-list/meta.json +2 -0
  44. package/components/pricing/Pricing.astro +15 -11
  45. package/components/pricing/meta.json +3 -1
  46. package/components/search-box/SearchBox.astro +4 -1
  47. package/components/search-box/meta.json +1 -1
  48. package/components/slider/Slider.astro +4 -1
  49. package/components/slider/SliderTrack.astro +11 -5
  50. package/components/slider/meta.json +1 -1
  51. package/components/social-links/SocialLinks.astro +2 -1
  52. package/components/social-links/meta.json +3 -2
  53. package/components/split/Split.astro +9 -5
  54. package/components/split/meta.json +3 -1
  55. package/components/stats/Stats.astro +11 -7
  56. package/components/stats/meta.json +3 -1
  57. package/components/steps/Steps.astro +9 -5
  58. package/components/steps/meta.json +3 -1
  59. package/components/tabs/Tabs.astro +8 -4
  60. package/components/tabs/meta.json +1 -1
  61. package/components/team/Team.astro +10 -6
  62. package/components/team/meta.json +4 -2
  63. package/components/testimonial/Testimonial.astro +9 -5
  64. package/components/testimonial/fixtures.json +1 -1
  65. package/components/testimonial/meta.json +3 -1
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -81,6 +81,22 @@ The starter sets it to the accent; a site whose accent does not reach 4.5:1 on i
81
81
  surface sets a readable one. A dark section (`tone="inverse"`) reads
82
82
  `--color-accent-text-inverse` in its place.
83
83
 
84
+ Quieter text (an intro, a caption, a role, a footer link) is a colour role, never
85
+ opacity: dimmed text lays its colour over the band, and on a mid-tone band that falls
86
+ under 4.5:1. It reads `--color-ink-muted` on a light band, `--color-ink-muted-inverse`
87
+ on a dark one (the starter mixes the surface toward the ink; without it, the band's own
88
+ text colour) and the fill's own ink on an accent or brand band. A light surface inside
89
+ a band (a card, a form panel, a menu) is back on `--color-ink-muted`. The classes are
90
+ in `_shared/muted.ts`.
91
+
92
+ ### Section hooks
93
+
94
+ Every section component renders its root with an `id` and `data-kit-section="<component id>"`
95
+ (`hero`, `card-grid`, `slider`, …), so a site's theme or a design layer can style one
96
+ kind of section, or one section, without touching the component. The id is the one the
97
+ page gives, or the component id counted per page (`hero`, then `hero-2`), the same on
98
+ every build.
99
+
84
100
  A migrated site also carries its theme's type and shape, and components read
85
101
  these with a fallback to the kit's own values, so a site without them looks
86
102
  unchanged:
package/catalog.json CHANGED
@@ -23,6 +23,7 @@
23
23
  "--radius-control",
24
24
  "--radius-image",
25
25
  "--spacing-gutter",
26
+ "--color-ink-muted-inverse",
26
27
  "--text-nav",
27
28
  "--text-footer",
28
29
  "--text-heading-1",
@@ -103,7 +104,9 @@
103
104
  "Button.astro",
104
105
  "KitImage.astro",
105
106
  "Section.astro",
107
+ "instance-id.ts",
106
108
  "measured.ts",
109
+ "muted.ts",
107
110
  "safe-url.ts",
108
111
  "types.ts"
109
112
  ],
@@ -457,7 +460,9 @@
457
460
  "behaviors.ts",
458
461
  "embed-frame.ts",
459
462
  "embed-target.ts",
463
+ "instance-id.ts",
460
464
  "measured.ts",
465
+ "muted.ts",
461
466
  "safe-url.ts",
462
467
  "types.ts"
463
468
  ],
@@ -643,6 +648,11 @@
643
648
  "description": "The section's minimum height."
644
649
  }
645
650
  }
651
+ },
652
+ "id": {
653
+ "type": "string",
654
+ "content": false,
655
+ "description": "Anchor id of the section. Without one, `behavior`, `behavior-2`, … in render order, so every section on a page has its own."
646
656
  }
647
657
  },
648
658
  "slots": {},
@@ -742,7 +752,9 @@
742
752
  "KitImage.astro",
743
753
  "Section.astro",
744
754
  "image.ts",
755
+ "instance-id.ts",
745
756
  "measured.ts",
757
+ "muted.ts",
746
758
  "safe-url.ts",
747
759
  "types.ts"
748
760
  ],
@@ -964,6 +976,11 @@
964
976
  "description": "The section's minimum height."
965
977
  }
966
978
  }
979
+ },
980
+ "id": {
981
+ "type": "string",
982
+ "content": false,
983
+ "description": "Anchor id of the section; the title gets `<id>-title`. Without one, `card-grid`, `card-grid-2`, … in render order, so every section on a page has its own."
967
984
  }
968
985
  },
969
986
  "slots": {},
@@ -1087,7 +1104,9 @@
1087
1104
  "uses": [],
1088
1105
  "shared": [
1089
1106
  "Section.astro",
1107
+ "instance-id.ts",
1090
1108
  "measured.ts",
1109
+ "muted.ts",
1091
1110
  "safe-url.ts",
1092
1111
  "types.ts"
1093
1112
  ],
@@ -1319,7 +1338,9 @@
1319
1338
  "shared": [
1320
1339
  "KitImage.astro",
1321
1340
  "Section.astro",
1341
+ "instance-id.ts",
1322
1342
  "measured.ts",
1343
+ "muted.ts",
1323
1344
  "safe-url.ts",
1324
1345
  "types.ts"
1325
1346
  ],
@@ -1646,7 +1667,9 @@
1646
1667
  "Button.astro",
1647
1668
  "KitImage.astro",
1648
1669
  "Section.astro",
1670
+ "instance-id.ts",
1649
1671
  "measured.ts",
1672
+ "muted.ts",
1650
1673
  "safe-url.ts",
1651
1674
  "types.ts"
1652
1675
  ],
@@ -1901,7 +1924,7 @@
1901
1924
  "id": {
1902
1925
  "type": "string",
1903
1926
  "content": false,
1904
- "description": "Anchor id of the section; the heading gets `<id>-title`."
1927
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `cta`, `cta-2`, … in render order, so every section on a page has its own."
1905
1928
  }
1906
1929
  },
1907
1930
  "slots": {},
@@ -1947,7 +1970,9 @@
1947
1970
  "shared": [
1948
1971
  "Button.astro",
1949
1972
  "Section.astro",
1973
+ "instance-id.ts",
1950
1974
  "measured.ts",
1975
+ "muted.ts",
1951
1976
  "safe-url.ts",
1952
1977
  "types.ts"
1953
1978
  ],
@@ -2338,7 +2363,9 @@
2338
2363
  "uses": [],
2339
2364
  "shared": [
2340
2365
  "Section.astro",
2366
+ "instance-id.ts",
2341
2367
  "measured.ts",
2368
+ "muted.ts",
2342
2369
  "safe-url.ts",
2343
2370
  "types.ts"
2344
2371
  ],
@@ -2611,7 +2638,9 @@
2611
2638
  "shared": [
2612
2639
  "KitImage.astro",
2613
2640
  "Section.astro",
2641
+ "instance-id.ts",
2614
2642
  "measured.ts",
2643
+ "muted.ts",
2615
2644
  "safe-url.ts",
2616
2645
  "types.ts"
2617
2646
  ],
@@ -2823,7 +2852,7 @@
2823
2852
  "id": {
2824
2853
  "type": "string",
2825
2854
  "content": false,
2826
- "description": "Anchor id of the section; the heading gets `<id>-title`."
2855
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `feature-list`, `feature-list-2`, … in render order, so every section on a page has its own."
2827
2856
  }
2828
2857
  },
2829
2858
  "slots": {},
@@ -2871,6 +2900,7 @@
2871
2900
  "category": "chrome",
2872
2901
  "uses": [],
2873
2902
  "shared": [
2903
+ "muted.ts",
2874
2904
  "safe-url.ts",
2875
2905
  "types.ts"
2876
2906
  ],
@@ -3093,7 +3123,9 @@
3093
3123
  "KitImage.astro",
3094
3124
  "Section.astro",
3095
3125
  "image.ts",
3126
+ "instance-id.ts",
3096
3127
  "measured.ts",
3128
+ "muted.ts",
3097
3129
  "safe-url.ts",
3098
3130
  "types.ts"
3099
3131
  ],
@@ -3389,6 +3421,8 @@
3389
3421
  "shared": [
3390
3422
  "Button.astro",
3391
3423
  "KitImage.astro",
3424
+ "instance-id.ts",
3425
+ "muted.ts",
3392
3426
  "safe-url.ts",
3393
3427
  "types.ts"
3394
3428
  ],
@@ -3619,7 +3653,9 @@
3619
3653
  "Button.astro",
3620
3654
  "KitImage.astro",
3621
3655
  "Section.astro",
3656
+ "instance-id.ts",
3622
3657
  "measured.ts",
3658
+ "muted.ts",
3623
3659
  "safe-url.ts",
3624
3660
  "types.ts"
3625
3661
  ],
@@ -3809,7 +3845,7 @@
3809
3845
  "id": {
3810
3846
  "type": "string",
3811
3847
  "content": false,
3812
- "description": "Anchor id of the section; the heading gets `<id>-title`."
3848
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `hero`, `hero-2`, … in render order, so every section on a page has its own."
3813
3849
  },
3814
3850
  "minHeight": {
3815
3851
  "type": "integer",
@@ -3976,6 +4012,7 @@
3976
4012
  "category": "chrome",
3977
4013
  "uses": [],
3978
4014
  "shared": [
4015
+ "muted.ts",
3979
4016
  "safe-url.ts"
3980
4017
  ],
3981
4018
  "variants": {
@@ -4057,7 +4094,9 @@
4057
4094
  "KitImage.astro",
4058
4095
  "Section.astro",
4059
4096
  "image.ts",
4097
+ "instance-id.ts",
4060
4098
  "measured.ts",
4099
+ "muted.ts",
4061
4100
  "safe-url.ts",
4062
4101
  "types.ts"
4063
4102
  ],
@@ -4266,7 +4305,7 @@
4266
4305
  "id": {
4267
4306
  "type": "string",
4268
4307
  "content": false,
4269
- "description": "Anchor id of the section; the heading gets `<id>-title`."
4308
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `logo-cloud`, `logo-cloud-2`, … in render order, so every section on a page has its own."
4270
4309
  }
4271
4310
  },
4272
4311
  "slots": {},
@@ -4312,6 +4351,8 @@
4312
4351
  "category": "chrome",
4313
4352
  "uses": [],
4314
4353
  "shared": [
4354
+ "instance-id.ts",
4355
+ "muted.ts",
4315
4356
  "safe-url.ts",
4316
4357
  "types.ts"
4317
4358
  ],
@@ -4437,7 +4478,9 @@
4437
4478
  "uses": [],
4438
4479
  "shared": [
4439
4480
  "Section.astro",
4481
+ "instance-id.ts",
4440
4482
  "measured.ts",
4483
+ "muted.ts",
4441
4484
  "safe-url.ts",
4442
4485
  "types.ts"
4443
4486
  ],
@@ -4927,7 +4970,9 @@
4927
4970
  ],
4928
4971
  "shared": [
4929
4972
  "Section.astro",
4973
+ "instance-id.ts",
4930
4974
  "measured.ts",
4975
+ "muted.ts",
4931
4976
  "safe-url.ts",
4932
4977
  "types.ts"
4933
4978
  ],
@@ -5247,7 +5292,9 @@
5247
5292
  "shared": [
5248
5293
  "Button.astro",
5249
5294
  "Section.astro",
5295
+ "instance-id.ts",
5250
5296
  "measured.ts",
5297
+ "muted.ts",
5251
5298
  "safe-url.ts",
5252
5299
  "types.ts"
5253
5300
  ],
@@ -5385,7 +5432,7 @@
5385
5432
  "id": {
5386
5433
  "type": "string",
5387
5434
  "content": false,
5388
- "description": "Anchor id of the section; the heading gets `<id>-title`."
5435
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `pricing`, `pricing-2`, … in render order, so every section on a page has its own."
5389
5436
  },
5390
5437
  "compact": {
5391
5438
  "type": "boolean",
@@ -5660,6 +5707,7 @@
5660
5707
  "category": "chrome",
5661
5708
  "uses": [],
5662
5709
  "shared": [
5710
+ "instance-id.ts",
5663
5711
  "safe-url.ts"
5664
5712
  ],
5665
5713
  "variants": {
@@ -5747,7 +5795,9 @@
5747
5795
  "embed-frame.ts",
5748
5796
  "embed-target.ts",
5749
5797
  "image.ts",
5798
+ "instance-id.ts",
5750
5799
  "measured.ts",
5800
+ "muted.ts",
5751
5801
  "safe-url.ts",
5752
5802
  "types.ts"
5753
5803
  ],
@@ -6102,8 +6152,9 @@
6102
6152
  "category": "chrome",
6103
6153
  "uses": [],
6104
6154
  "shared": [
6105
- "social.ts",
6106
- "safe-url.ts"
6155
+ "muted.ts",
6156
+ "safe-url.ts",
6157
+ "social.ts"
6107
6158
  ],
6108
6159
  "variants": {
6109
6160
  "display": {
@@ -6205,7 +6256,9 @@
6205
6256
  "Button.astro",
6206
6257
  "KitImage.astro",
6207
6258
  "Section.astro",
6259
+ "instance-id.ts",
6208
6260
  "measured.ts",
6261
+ "muted.ts",
6209
6262
  "safe-url.ts",
6210
6263
  "types.ts"
6211
6264
  ],
@@ -6533,7 +6586,7 @@
6533
6586
  "id": {
6534
6587
  "type": "string",
6535
6588
  "content": false,
6536
- "description": "Anchor id of the section; the heading gets `<id>-title`."
6589
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `split`, `split-2`, … in render order, so every section on a page has its own."
6537
6590
  },
6538
6591
  "sticky": {
6539
6592
  "type": "boolean",
@@ -6605,7 +6658,9 @@
6605
6658
  "embed-frame.ts",
6606
6659
  "embed-target.ts",
6607
6660
  "image.ts",
6661
+ "instance-id.ts",
6608
6662
  "measured.ts",
6663
+ "muted.ts",
6609
6664
  "safe-url.ts",
6610
6665
  "types.ts"
6611
6666
  ],
@@ -6854,7 +6909,7 @@
6854
6909
  "id": {
6855
6910
  "type": "string",
6856
6911
  "content": false,
6857
- "description": "Anchor id of the section; the heading gets `<id>-title`."
6912
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `stats`, `stats-2`, … in render order, so every section on a page has its own."
6858
6913
  }
6859
6914
  },
6860
6915
  "slots": {},
@@ -6905,7 +6960,9 @@
6905
6960
  "KitImage.astro",
6906
6961
  "Section.astro",
6907
6962
  "image.ts",
6963
+ "instance-id.ts",
6908
6964
  "measured.ts",
6965
+ "muted.ts",
6909
6966
  "safe-url.ts",
6910
6967
  "types.ts"
6911
6968
  ],
@@ -7016,7 +7073,7 @@
7016
7073
  "id": {
7017
7074
  "type": "string",
7018
7075
  "content": false,
7019
- "description": "Anchor id of the section; the heading gets `<id>-title`."
7076
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `steps`, `steps-2`, … in render order, so every section on a page has its own."
7020
7077
  },
7021
7078
  "currentLabel": {
7022
7079
  "type": "string",
@@ -7185,7 +7242,9 @@
7185
7242
  "Button.astro",
7186
7243
  "Section.astro",
7187
7244
  "SectionHeader.astro",
7245
+ "instance-id.ts",
7188
7246
  "measured.ts",
7247
+ "muted.ts",
7189
7248
  "safe-url.ts",
7190
7249
  "types.ts"
7191
7250
  ],
@@ -7441,9 +7500,11 @@
7441
7500
  "KitImage.astro",
7442
7501
  "Section.astro",
7443
7502
  "image.ts",
7503
+ "instance-id.ts",
7444
7504
  "measured.ts",
7445
- "social.ts",
7505
+ "muted.ts",
7446
7506
  "safe-url.ts",
7507
+ "social.ts",
7447
7508
  "types.ts"
7448
7509
  ],
7449
7510
  "variants": {
@@ -7564,7 +7625,7 @@
7564
7625
  "id": {
7565
7626
  "type": "string",
7566
7627
  "content": false,
7567
- "description": "Anchor id of the section; the heading gets `<id>-title`."
7628
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `team`, `team-2`, … in render order, so every section on a page has its own."
7568
7629
  },
7569
7630
  "bioClamp": {
7570
7631
  "type": "integer",
@@ -7748,7 +7809,9 @@
7748
7809
  "KitImage.astro",
7749
7810
  "Section.astro",
7750
7811
  "image.ts",
7812
+ "instance-id.ts",
7751
7813
  "measured.ts",
7814
+ "muted.ts",
7752
7815
  "safe-url.ts",
7753
7816
  "types.ts"
7754
7817
  ],
@@ -7990,7 +8053,7 @@
7990
8053
  "id": {
7991
8054
  "type": "string",
7992
8055
  "content": false,
7993
- "description": "Anchor id of the section; the heading gets `<id>-title`."
8056
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `testimonial`, `testimonial-2`, … in render order, so every section on a page has its own."
7994
8057
  },
7995
8058
  "ratingLabel": {
7996
8059
  "type": "string",
@@ -4,6 +4,7 @@
4
4
  // their order.
5
5
  import { tv } from 'tailwind-variants'
6
6
  import { measuredFrame } from './measured'
7
+ import { mutedAccent, mutedBrand, mutedInverse, mutedReset } from './muted'
7
8
  import { safeSrc } from './safe-url'
8
9
  import type { MeasuredStyle } from './types'
9
10
 
@@ -11,13 +12,14 @@ const section = tv({
11
12
  base: '',
12
13
  variants: {
13
14
  tone: {
14
- default: 'bg-surface text-ink',
15
- muted: 'bg-surface-muted text-ink',
16
- // A dark band reads the accent's text colour made for it (`--color-accent-text-inverse`).
17
- inverse: 'bg-ink text-surface [--color-accent-text:var(--color-accent-text-inverse)]',
18
- accent: 'bg-accent text-accent-ink',
15
+ default: `bg-surface text-ink ${mutedReset}`,
16
+ muted: `bg-surface-muted text-ink ${mutedReset}`,
17
+ // A dark band reads the accent's text colour made for it (`--color-accent-text-inverse`), and its quieter text
18
+ // the muted role made for it (`muted.ts`).
19
+ inverse: `bg-ink text-surface [--color-accent-text:var(--color-accent-text-inverse)] ${mutedInverse}`,
20
+ accent: `bg-accent text-accent-ink ${mutedAccent}`,
19
21
  // The site's brand colour where it is not the accent (a band the source paints in the builder's own colour).
20
- brand: 'bg-brand text-brand-ink',
22
+ brand: `bg-brand text-brand-ink ${mutedBrand}`,
21
23
  // No fill of its own: the page's background and text colour show through.
22
24
  none: '',
23
25
  },
@@ -42,15 +44,17 @@ interface Props {
42
44
  id?: string | undefined
43
45
  class?: string | undefined
44
46
  'aria-labelledby'?: string | undefined
47
+ /** The component this section is (`hero`, `card-grid`): `data-kit-section`'s value, the hook a theme or a design layer styles one kind of section by. */
48
+ kit?: string | undefined
45
49
  /** The source section's measured style (`MeasuredStyle`): frame width, padding, fill, image, columns, gap. Wins over `tone`. */
46
50
  measured?: MeasuredStyle | undefined
47
51
  }
48
52
 
49
- const { tone: given, spacing, width = 'page', as: Tag = 'section', id, class: className, 'aria-labelledby': labelledBy, measured } = Astro.props
53
+ const { tone: given, spacing, width = 'page', as: Tag = 'section', id, class: className, 'aria-labelledby': labelledBy, kit, measured } = Astro.props
50
54
  const m = measuredFrame(measured)
51
55
  // A measured fill is the section's own colour, its text set by the measured tone; a tone alone picks the kit's band.
52
56
  const tone = m.color ? 'none' : m.tone === 'dark' ? 'inverse' : m.tone === 'light' ? 'default' : given
53
- const text = m.color ? (m.tone === 'dark' ? 'text-surface [--color-accent-text:var(--color-accent-text-inverse)]' : 'text-ink') : undefined
57
+ const text = m.color ? (m.tone === 'dark' ? `text-surface [--color-accent-text:var(--color-accent-text-inverse)] ${mutedInverse}` : `text-ink ${mutedReset}`) : undefined
54
58
  const layered = m.image || m.overlay ? 'relative isolate overflow-hidden' : undefined
55
59
  const style = [m.color && `background-color: ${m.color}`, m.style].filter(Boolean).join('; ') || undefined
56
60
  // The reading column reads its own width token.
@@ -58,7 +62,7 @@ const frameStyle = width === 'prose' ? m.frameStyle?.replace('--container-page:'
58
62
  const backdrop = safeSrc(m.image?.src)
59
63
  const dataTone = m.color ? (m.tone === 'dark' ? 'inverse' : 'default') : tone ?? 'default'
60
64
  ---
61
- <Tag id={id} aria-labelledby={labelledBy} class={section({ tone, spacing, class: [text, layered, className].filter(Boolean).join(' ') || undefined })} style={style} data-tone={dataTone} data-kit-section data-kit-spacing={spacing ?? 'md'} data-kit-measured={measured ? '' : undefined}>
65
+ <Tag id={id} aria-labelledby={labelledBy} class={section({ tone, spacing, class: [text, layered, className].filter(Boolean).join(' ') || undefined })} style={style} data-tone={dataTone} data-kit-section={kit ?? ''} data-kit-spacing={spacing ?? 'md'} data-kit-measured={measured ? '' : undefined}>
62
66
  {m.image && backdrop && <img src={backdrop} alt="" loading="lazy" decoding="async" class="absolute inset-0 -z-10 h-full w-full" style={`object-fit: ${m.image.fit}${m.image.position ? `; object-position: ${m.image.position}` : ''}`} />}
63
67
  {m.overlay && <div class="absolute inset-0 -z-10" style={`background: ${m.overlay}`} aria-hidden="true" />}
64
68
  {width === 'none' ? <slot /> : <div class={width === 'prose' ? 'container-prose' : 'container-page'} style={frameStyle}><slot /></div>}
@@ -3,6 +3,7 @@
3
3
  // section components that have no header of their own use it; with none of the four given it draws nothing.
4
4
  import Button from './Button.astro'
5
5
  import type { ActionInput } from './types'
6
+ import { muted } from './muted'
6
7
 
7
8
  interface Props {
8
9
  heading?: string | undefined
@@ -19,9 +20,9 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3'
19
20
  ---
20
21
  {(heading || intro || eyebrow || actions.length > 0) && (
21
22
  <header class="mb-10 flex max-w-2xl flex-col gap-3">
22
- {eyebrow && <p class="text-sm font-semibold tracking-wide uppercase opacity-80">{eyebrow}</p>}
23
+ {eyebrow && <p class:list={['text-sm font-semibold tracking-wide uppercase', muted]}>{eyebrow}</p>}
23
24
  {heading && <Heading id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</Heading>}
24
- {intro && <p class="text-lg opacity-80" data-kit-text>{intro}</p>}
25
+ {intro && <p class:list={['text-lg', muted]} data-kit-text>{intro}</p>}
25
26
  {actions.length > 0 && <div class="mt-2 flex flex-wrap gap-3">{actions.map(action => <Button action={action} />)}</div>}
26
27
  </header>
27
28
  )}
@@ -0,0 +1,24 @@
1
+ // A component's id when the page gives none. Its tabs, lightboxes, labels and buttons point at elements by id
2
+ // (`aria-controls`, `popovertarget`, `for`), so two instances on one page that both took the same default (two galleries,
3
+ // two FAQs, two newsletters) would point into each other: a click in the second opened the first one's image. Each
4
+ // default is taken once per page instead: `gallery`, then `gallery-2`, `gallery-3`, in the order the page renders
5
+ // them, the same on every build. An id the page gives is kept as it is, and a default steps around it.
6
+
7
+ const taken = new WeakMap<object, Set<string>>()
8
+
9
+ /** `given` when set, else `base`, `base-2`, … — the first one not yet used on this page (`page` = `Astro.locals`). */
10
+ export function instanceId(page: object, given: string | undefined, base: string): string {
11
+ let ids = taken.get(page)
12
+ if (!ids) {
13
+ ids = new Set()
14
+ taken.set(page, ids)
15
+ }
16
+ if (given) {
17
+ ids.add(given)
18
+ return given
19
+ }
20
+ let id = base
21
+ for (let n = 2; ids.has(id); n++) id = `${base}-${n}`
22
+ ids.add(id)
23
+ return id
24
+ }
@@ -0,0 +1,19 @@
1
+ // Quieter text (an intro, a caption, a role, a footer link) in a colour role, never a dimmed ink: opacity lays the
2
+ // text over whatever is behind it, and on a mid-tone band that falls under 4.5:1. The role is the one made for the
3
+ // band the text sits on. A band sets `--kit-muted` (dark: `--color-ink-muted-inverse`, a fill: its own ink), a light
4
+ // surface inside it (a card, a form panel, a drawer) clears it again, and without either it is `--color-ink-muted`.
5
+
6
+ /** Quieter text: the band's muted role. */
7
+ export const muted = 'text-[color:var(--kit-muted,var(--color-ink-muted))]'
8
+
9
+ /** On a dark band: the muted role made for it, or the band's own text where the site's theme has none. */
10
+ export const mutedInverse = '[--kit-muted:var(--color-ink-muted-inverse,var(--color-surface))]'
11
+
12
+ /** On the accent fill: no muted role is made for it, so quieter text there is the fill's own ink. */
13
+ export const mutedAccent = '[--kit-muted:var(--color-accent-ink)]'
14
+
15
+ /** On the brand fill: the fill's own ink, as on the accent. */
16
+ export const mutedBrand = '[--kit-muted:var(--color-brand-ink)]'
17
+
18
+ /** A light surface inside a band (a card, a form panel, a drawer): back to the page's muted role. */
19
+ export const mutedReset = '[--kit-muted:initial]'
@@ -9,6 +9,8 @@ import Section, { type Tone } from '../_shared/Section.astro'
9
9
  import { measuredGrid } from '../_shared/measured'
10
10
  import type { ActionInput, ImageInput, MeasuredStyle } from '../_shared/types'
11
11
  import { safeHref } from '../_shared/safe-url'
12
+ import { instanceId } from '../_shared/instance-id'
13
+ import { muted } from '../_shared/muted'
12
14
 
13
15
  const band = tv({
14
16
  slots: {
@@ -16,16 +18,16 @@ const band = tv({
16
18
  copy: 'flex max-w-3xl flex-col gap-4',
17
19
  eyebrow: 'text-sm font-semibold tracking-wide text-accent-text uppercase',
18
20
  heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
19
- lead: 'text-lg text-pretty opacity-85',
21
+ lead: `text-lg text-pretty ${muted}`,
20
22
  media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
21
23
  list: 'grid items-start gap-6 md:gap-8',
22
24
  item: 'relative flex flex-col gap-3 rounded-[var(--radius-card)]',
23
25
  itemMedia: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
24
26
  itemTitle: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
25
27
  itemValue: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight',
26
- itemMeta: 'text-sm opacity-75',
27
- itemText: 'opacity-80',
28
- itemPoints: 'flex list-disc flex-col gap-1 ps-5 opacity-80',
28
+ itemMeta: `text-sm ${muted}`,
29
+ itemText: muted,
30
+ itemPoints: `flex list-disc flex-col gap-1 ps-5 ${muted}`,
29
31
  actions: 'flex flex-wrap gap-3',
30
32
  },
31
33
  variants: {
@@ -42,7 +44,7 @@ const band = tv({
42
44
  center: { copy: 'mx-auto items-center text-center', actions: 'justify-center', item: 'items-center text-center' },
43
45
  },
44
46
  // Accent backgrounds have the least contrast headroom: text on them is never dimmed.
45
- tone: { default: {}, muted: {}, inverse: {}, accent: { lead: 'opacity-100', itemText: 'opacity-100', itemMeta: 'opacity-100', itemPoints: 'opacity-100', eyebrow: 'text-current' } },
47
+ tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'text-current' } },
46
48
  },
47
49
  defaultVariants: { layout: 'stack', columns: '3', align: 'start', tone: 'default' },
48
50
  })
@@ -89,10 +91,11 @@ const aligned = align ?? (measured?.align === 'center' ? 'center' : undefined)
89
91
  const s = band({ layout: media ? layout : 'stack', columns, align: aligned, tone })
90
92
  const Heading = `h${headingLevel}` as 'h2' | 'h3'
91
93
  const ItemHeading = `h${heading ? headingLevel + 1 : headingLevel}` as 'h2' | 'h3' | 'h4'
92
- const headingId = heading ? (id ? `${id}-title` : `band-${heading.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`) : undefined
94
+ // Two bands with the same heading on one page get two ids (`band-<heading>`, then `band-<heading>-2`).
95
+ const headingId = heading ? (id ? `${id}-title` : instanceId(Astro.locals, undefined, `band-${heading.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`)) : undefined
93
96
  const hasCopy = Boolean(heading || eyebrow || lead || actions.length)
94
97
  ---
95
- <Section tone={tone} id={id} aria-labelledby={headingId} measured={measured}>
98
+ <Section kit="band" tone={tone} id={id} aria-labelledby={headingId} measured={measured}>
96
99
  <div class="flex flex-col gap-10">
97
100
  {(hasCopy || media) && (
98
101
  <div class={s.grid()}>
@@ -8,7 +8,9 @@
8
8
  "Button.astro",
9
9
  "KitImage.astro",
10
10
  "Section.astro",
11
+ "instance-id.ts",
11
12
  "measured.ts",
13
+ "muted.ts",
12
14
  "safe-url.ts",
13
15
  "types.ts"
14
16
  ],
@@ -9,6 +9,7 @@
9
9
  // nothing hidden. Only a page that has an island loads the module (Astro bundles a component's script once, for the
10
10
  // pages that use it).
11
11
  import Section from '../_shared/Section.astro'
12
+ import { instanceId } from '../_shared/instance-id'
12
13
  import type { MeasuredStyle } from '../_shared/types'
13
14
 
14
15
  interface Props {
@@ -29,9 +30,13 @@ interface Props {
29
30
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
30
31
  /** The source section's measured style (`MeasuredStyle`). */
31
32
  measured?: MeasuredStyle | undefined
33
+ /** Section id, unique per page; without one `behavior`, `behavior-2`, … in render order. */
34
+ id?: string | undefined
32
35
  }
33
36
 
34
- const { kind, html, items, title, panel, fact, previousLabel, nextLabel, closeLabel, tone, measured } = Astro.props
37
+ const { kind, html, items, title, panel, fact, previousLabel, nextLabel, closeLabel, tone, measured, id: givenId } = Astro.props
38
+ // Every section has an id, so a theme or a design layer can address this one: the page's, or `behavior`, `behavior-2`, … in render order.
39
+ const id = instanceId(Astro.locals, givenId, 'behavior')
35
40
  // Selectors and ids go into attributes only, never into markup or a script; an empty one is left out.
36
41
  const attr = (value: string | undefined): string | undefined => (value && value.length <= 300 ? value : undefined)
37
42
  const factId = fact && /^[\w:.-]{1,120}$/.test(fact) ? fact : undefined
@@ -39,7 +44,7 @@ const factId = fact && /^[\w:.-]{1,120}$/.test(fact) ? fact : undefined
39
44
  const NOSCRIPT = '<style>.cr-behavior dialog:not([open]){display:block;position:static;margin:0;max-width:none}</style>'
40
45
  ---
41
46
  {html && (
42
- <Section tone={tone} measured={measured}>
47
+ <Section kit="behavior" tone={tone} id={id} measured={measured}>
43
48
  <div
44
49
  class="cr-behavior"
45
50
  data-cr-behavior={kind}
@@ -39,7 +39,7 @@
39
39
  "kind": "disclosure",
40
40
  "fact": "interactive:fx3",
41
41
  "tone": "inverse",
42
- "html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx3-a\">Do you ship abroad?</button></h2><div id=\"cr-fx3-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
42
+ "html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx5-a\">Do you ship abroad?</button></h2><div id=\"cr-fx5-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
43
43
  }
44
44
  },
45
45
  {
@@ -48,7 +48,7 @@
48
48
  "kind": "disclosure",
49
49
  "fact": "interactive:fx3",
50
50
  "tone": "accent",
51
- "html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx3-a\">Do you ship abroad?</button></h2><div id=\"cr-fx3-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
51
+ "html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx6-a\">Do you ship abroad?</button></h2><div id=\"cr-fx6-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
52
52
  }
53
53
  },
54
54
  {"name": "video-link", "props": {"kind": "anchor", "fact": "interactive:fx5", "html": "<h2>Studio tour</h2><p>A walk through the workshop.</p><a href=\"https://www.youtube.com/watch?v=aqz-KE-bpKQ\" target=\"_blank\" rel=\"noopener noreferrer\" data-cr-video=\"youtube:aqz-KE-bpKQ\"><img src=\"/kit-fixtures/photo-2.svg\" alt=\"\" loading=\"lazy\" width=\"1200\" height=\"800\"><span>Studio tour</span></a>"}},