@contentrain/astro-kit 0.43.0 → 0.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/catalog.json +84 -16
- package/components/gallery/Gallery.astro +19 -9
- package/components/gallery/GalleryLightbox.astro +110 -0
- package/components/gallery/fixtures.json +52 -0
- package/components/gallery/meta.json +24 -5
- package/components/pricing/Pricing.astro +38 -3
- package/components/pricing/fixtures.json +84 -0
- package/components/pricing/meta.json +50 -9
- package/package.json +1 -1
package/catalog.json
CHANGED
|
@@ -3164,7 +3164,7 @@
|
|
|
3164
3164
|
{
|
|
3165
3165
|
"id": "gallery",
|
|
3166
3166
|
"name": "Gallery",
|
|
3167
|
-
"description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), opens the image file, or does nothing.",
|
|
3167
|
+
"description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), steps through the set in a lightbox viewer, opens the image file, or does nothing.",
|
|
3168
3168
|
"category": "section",
|
|
3169
3169
|
"uses": [],
|
|
3170
3170
|
"shared": [
|
|
@@ -3203,10 +3203,11 @@
|
|
|
3203
3203
|
"options": [
|
|
3204
3204
|
"popover",
|
|
3205
3205
|
"link",
|
|
3206
|
-
"none"
|
|
3206
|
+
"none",
|
|
3207
|
+
"lightbox"
|
|
3207
3208
|
],
|
|
3208
3209
|
"default": "popover",
|
|
3209
|
-
"description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only."
|
|
3210
|
+
"description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only. lightbox: each thumbnail links to its large file (full, else the image); with script a click opens one modal viewer for the whole set, with previous / next, ←/→, a counter and the caption (a source lightbox: Elementor's slideshow group, a WordPress gallery with a lightbox plugin). The viewer and its script are printed only for this option."
|
|
3210
3211
|
},
|
|
3211
3212
|
"aspect": {
|
|
3212
3213
|
"options": [
|
|
@@ -3287,6 +3288,24 @@
|
|
|
3287
3288
|
"content": false,
|
|
3288
3289
|
"description": "Text of the viewer's close button (ui-strings gallery.close)."
|
|
3289
3290
|
},
|
|
3291
|
+
"prevLabel": {
|
|
3292
|
+
"type": "string",
|
|
3293
|
+
"default": "Previous image",
|
|
3294
|
+
"content": false,
|
|
3295
|
+
"description": "Name of the lightbox viewer's previous button (ui-strings slider.prev)."
|
|
3296
|
+
},
|
|
3297
|
+
"nextLabel": {
|
|
3298
|
+
"type": "string",
|
|
3299
|
+
"default": "Next image",
|
|
3300
|
+
"content": false,
|
|
3301
|
+
"description": "Name of the lightbox viewer's next button (ui-strings slider.next)."
|
|
3302
|
+
},
|
|
3303
|
+
"ofLabel": {
|
|
3304
|
+
"type": "string",
|
|
3305
|
+
"default": "of",
|
|
3306
|
+
"content": false,
|
|
3307
|
+
"description": "The word of the lightbox viewer's counter between position and total (\"2 of 5\"; ui-strings slider.of)."
|
|
3308
|
+
},
|
|
3290
3309
|
"tone": {
|
|
3291
3310
|
"type": "select",
|
|
3292
3311
|
"options": [
|
|
@@ -3427,7 +3446,7 @@
|
|
|
3427
3446
|
}
|
|
3428
3447
|
},
|
|
3429
3448
|
"slots": {},
|
|
3430
|
-
"js": "
|
|
3449
|
+
"js": "vanilla",
|
|
3431
3450
|
"dependencies": {},
|
|
3432
3451
|
"sources": {
|
|
3433
3452
|
"gutenberg": [
|
|
@@ -3444,9 +3463,10 @@
|
|
|
3444
3463
|
"classic/gallery"
|
|
3445
3464
|
]
|
|
3446
3465
|
},
|
|
3447
|
-
"a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s. A scrolling row is a labelled region with tabindex 0 so the keyboard can scroll it; thumbnail buttons are named \"Enlarge: …\". The viewer
|
|
3466
|
+
"a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s. A scrolling row is a labelled region with tabindex 0 so the keyboard can scroll it; thumbnail buttons are named \"Enlarge: …\". The popover viewer shows one image. In lightbox mode each thumbnail is a link to its large file, named \"Enlarge: …\"; with script it opens one modal <dialog> named by the heading (or the region label): focus moves into it and stays there (the page behind is inert), Escape or a click outside the image closes it and focus returns to the thumbnail, labelled previous / next buttons and ←/→ (mirrored right to left), Home and End step through the set, and the counter (\"2 of 5\") and caption are announced as they change. Nothing animates. Without script the links open the files. Other modes ship no script.",
|
|
3448
3467
|
"files": [
|
|
3449
|
-
"Gallery.astro"
|
|
3468
|
+
"Gallery.astro",
|
|
3469
|
+
"GalleryLightbox.astro"
|
|
3450
3470
|
],
|
|
3451
3471
|
"contentFields": {
|
|
3452
3472
|
"items": "items",
|
|
@@ -5416,7 +5436,7 @@
|
|
|
5416
5436
|
"radio"
|
|
5417
5437
|
],
|
|
5418
5438
|
"default": "none",
|
|
5419
|
-
"description": "radio: two labelled radios swap each plan's price for its priceAlt (monthly / yearly), with no script. The radio group is named from the section id, or a hash of the plans without one, so two toggles on a page stay independent."
|
|
5439
|
+
"description": "radio: two labelled radios swap each plan's price for its priceAlt (monthly / yearly), with no script. The radio group is named from the section id, or a hash of the plans without one, so two toggles on a page stay independent. A plan's priceWas, priceNote and priceFoot swap for their Alt values the same way, and billingAltNote shows only with the second option."
|
|
5420
5440
|
}
|
|
5421
5441
|
},
|
|
5422
5442
|
"props": {
|
|
@@ -5487,12 +5507,42 @@
|
|
|
5487
5507
|
"priceAlt": {
|
|
5488
5508
|
"type": "string",
|
|
5489
5509
|
"label": "Price, other period",
|
|
5490
|
-
"description": "The price for the other billing period (\"€150\"), shown when the toggle is on its second option."
|
|
5510
|
+
"description": "The price for the other billing period (\"€150\"), shown when the toggle is on its second option. Without it the plan's price (and its struck price) shows under both options: a free plan, \"Contact us\", or one price for both periods. Its notes do not: they stay first-period lines."
|
|
5491
5511
|
},
|
|
5492
5512
|
"periodAlt": {
|
|
5493
5513
|
"type": "string",
|
|
5494
5514
|
"label": "Period, other period",
|
|
5495
5515
|
"description": "What the other price is per (\"/year\")."
|
|
5516
|
+
},
|
|
5517
|
+
"priceWas": {
|
|
5518
|
+
"type": "string",
|
|
5519
|
+
"label": "Original price",
|
|
5520
|
+
"description": "The original price, struck before the price (\"$35\"; cards layout)."
|
|
5521
|
+
},
|
|
5522
|
+
"priceWasAlt": {
|
|
5523
|
+
"type": "string",
|
|
5524
|
+
"label": "Original price, other period",
|
|
5525
|
+
"description": "The other period's struck original price, shown with priceAlt. No fallback: without it, the second option shows no struck price (priceWas belongs to the first period only, except on a plan with no priceAlt, whose whole price line shows under both)."
|
|
5526
|
+
},
|
|
5527
|
+
"priceNote": {
|
|
5528
|
+
"type": "string",
|
|
5529
|
+
"label": "Price note",
|
|
5530
|
+
"description": "One line under the price (\"USD for the first month\"; cards layout)."
|
|
5531
|
+
},
|
|
5532
|
+
"priceNoteAlt": {
|
|
5533
|
+
"type": "string",
|
|
5534
|
+
"label": "Price note, other period",
|
|
5535
|
+
"description": "The other period's line under the price, shown with priceAlt. No fallback: without it, the second option shows no line there (a note like \"for the first month\" would be wrong under the other price)."
|
|
5536
|
+
},
|
|
5537
|
+
"priceFoot": {
|
|
5538
|
+
"type": "string",
|
|
5539
|
+
"label": "Footnote",
|
|
5540
|
+
"description": "One line at the card's foot, above the button (\"Pay $0 today, $35 after the first month\"; cards layout)."
|
|
5541
|
+
},
|
|
5542
|
+
"priceFootAlt": {
|
|
5543
|
+
"type": "string",
|
|
5544
|
+
"label": "Footnote, other period",
|
|
5545
|
+
"description": "The other period's line at the card's foot, shown with priceAlt. No fallback: without it, the second option shows no line there."
|
|
5496
5546
|
}
|
|
5497
5547
|
}
|
|
5498
5548
|
},
|
|
@@ -5502,7 +5552,7 @@
|
|
|
5502
5552
|
"type": "string",
|
|
5503
5553
|
"default": "Most popular",
|
|
5504
5554
|
"content": false,
|
|
5505
|
-
"description": "Badge text on featured plans."
|
|
5555
|
+
"description": "Badge text on featured plans (ui-strings pricing.featured)."
|
|
5506
5556
|
},
|
|
5507
5557
|
"id": {
|
|
5508
5558
|
"type": "string",
|
|
@@ -5519,13 +5569,13 @@
|
|
|
5519
5569
|
"type": "string",
|
|
5520
5570
|
"default": "Included",
|
|
5521
5571
|
"content": false,
|
|
5522
|
-
"description": "Table cell text for readers where a plan has the feature."
|
|
5572
|
+
"description": "Table cell text for readers where a plan has the feature (ui-strings pricing.included)."
|
|
5523
5573
|
},
|
|
5524
5574
|
"notIncludedLabel": {
|
|
5525
5575
|
"type": "string",
|
|
5526
5576
|
"default": "Not included",
|
|
5527
5577
|
"content": false,
|
|
5528
|
-
"description": "Table cell text for readers where a plan lacks the feature."
|
|
5578
|
+
"description": "Table cell text for readers where a plan lacks the feature (ui-strings pricing.not_included)."
|
|
5529
5579
|
},
|
|
5530
5580
|
"measured": {
|
|
5531
5581
|
"type": "object",
|
|
@@ -5641,19 +5691,30 @@
|
|
|
5641
5691
|
"type": "string",
|
|
5642
5692
|
"default": "Monthly",
|
|
5643
5693
|
"content": false,
|
|
5644
|
-
"description": "First option of the billing toggle."
|
|
5694
|
+
"description": "First option of the billing toggle (ui-strings pricing.billing_main)."
|
|
5645
5695
|
},
|
|
5646
5696
|
"billingAltLabel": {
|
|
5647
5697
|
"type": "string",
|
|
5648
5698
|
"default": "Yearly",
|
|
5649
5699
|
"content": false,
|
|
5650
|
-
"description": "Second option of the billing toggle
|
|
5700
|
+
"description": "Second option of the billing toggle, the plans' priceAlt (ui-strings pricing.billing_alt)."
|
|
5651
5701
|
},
|
|
5652
5702
|
"billingLegend": {
|
|
5653
5703
|
"type": "string",
|
|
5654
5704
|
"default": "Billing period",
|
|
5655
5705
|
"content": false,
|
|
5656
|
-
"description": "Legend of the toggle, read to screen readers."
|
|
5706
|
+
"description": "Legend of the toggle, read to screen readers (ui-strings pricing.billing_legend)."
|
|
5707
|
+
},
|
|
5708
|
+
"billingAltNote": {
|
|
5709
|
+
"type": "string",
|
|
5710
|
+
"label": "Second-option note",
|
|
5711
|
+
"description": "A line under the billing toggle shown only while its second option is chosen (\"Save $70 by paying annually\")."
|
|
5712
|
+
},
|
|
5713
|
+
"wasLabel": {
|
|
5714
|
+
"type": "string",
|
|
5715
|
+
"default": "Was",
|
|
5716
|
+
"content": false,
|
|
5717
|
+
"description": "Read before a struck original price by screen readers, which do not announce the strike. 'Was' is only the kit default: the writer passes it in the site's language (ui-strings pricing.was)."
|
|
5657
5718
|
}
|
|
5658
5719
|
},
|
|
5659
5720
|
"slots": {},
|
|
@@ -5670,7 +5731,7 @@
|
|
|
5670
5731
|
"divi/et_pb_pricing_tables"
|
|
5671
5732
|
]
|
|
5672
5733
|
},
|
|
5673
|
-
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label. The table layout is a real <table> with a caption, column and row headers, and Included / Not included text in every cell; the billing toggle is a fieldset with a legend and labelled radios.",
|
|
5734
|
+
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label. The table layout is a real <table> with a caption, column and row headers, and Included / Not included text in every cell; the billing toggle is a fieldset with a legend and labelled radios. A struck original price is a <del> read as \"Was <price>\" (the strike alone is not announced); the period's notes swap with its price, hidden (display: none) for the period not chosen.",
|
|
5674
5735
|
"pattern": {
|
|
5675
5736
|
"summary": "repeated plan units: name + price + feature list + button",
|
|
5676
5737
|
"signals": [
|
|
@@ -5697,7 +5758,14 @@
|
|
|
5697
5758
|
"plans[].featured": "plans[].featured",
|
|
5698
5759
|
"plans[].badge": "plans[].badge",
|
|
5699
5760
|
"plans[].priceAlt": "plans[].price_alt",
|
|
5700
|
-
"plans[].periodAlt": "plans[].period_alt"
|
|
5761
|
+
"plans[].periodAlt": "plans[].period_alt",
|
|
5762
|
+
"plans[].priceWas": "plans[].price_was",
|
|
5763
|
+
"plans[].priceWasAlt": "plans[].price_was_alt",
|
|
5764
|
+
"plans[].priceNote": "plans[].price_note",
|
|
5765
|
+
"plans[].priceNoteAlt": "plans[].price_note_alt",
|
|
5766
|
+
"plans[].priceFoot": "plans[].price_foot",
|
|
5767
|
+
"plans[].priceFootAlt": "plans[].price_foot_alt",
|
|
5768
|
+
"billingAltNote": "billing_alt_note"
|
|
5701
5769
|
}
|
|
5702
5770
|
},
|
|
5703
5771
|
{
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// shows it large in a native popover — no script: `popovertarget` opens it,
|
|
4
4
|
// Escape and a click outside close it. `link` opens the image file instead.
|
|
5
5
|
// Layouts: grid, masonry, a scroll-snap `row`, and a `mosaic` whose first image
|
|
6
|
-
// spans 2×2. The popover shows one image;
|
|
6
|
+
// spans 2×2. The popover shows one image; `lightbox` steps through the whole set in one viewer (GalleryLightbox: a
|
|
7
|
+
// modal dialog and a small script, both printed only for that option), and each tile is a link to its large file.
|
|
7
8
|
import { tv } from 'tailwind-variants'
|
|
9
|
+
import GalleryLightbox from './GalleryLightbox.astro'
|
|
8
10
|
import KitImage from '../_shared/KitImage.astro'
|
|
9
11
|
import Section from '../_shared/Section.astro'
|
|
10
12
|
import { measuredGrid } from '../_shared/measured'
|
|
@@ -49,7 +51,7 @@ const gallery = tv({
|
|
|
49
51
|
captionStyle: { below: {}, overlay: { caption: 'absolute inset-x-0 bottom-0 m-0 bg-ink/70 p-2 text-surface md:translate-y-full md:transition-transform md:group-focus-within:translate-y-0 md:group-hover:translate-y-0' } },
|
|
50
52
|
hoverEffect: { none: {}, zoom: { thumb: 'motion-safe:group-hover:scale-105' } },
|
|
51
53
|
columns: { '2': { list: 'grid-cols-2' }, '3': { list: 'grid-cols-2 md:grid-cols-3' }, '4': { list: 'grid-cols-2 md:grid-cols-4' }, '5': { list: 'grid-cols-2 md:grid-cols-5' }, '6': { list: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6' } },
|
|
52
|
-
open: { popover: {}, link: {}, none: { frame: 'cursor-default' } },
|
|
54
|
+
open: { popover: {}, link: {}, lightbox: {}, none: { frame: 'cursor-default' } },
|
|
53
55
|
},
|
|
54
56
|
compoundVariants: [
|
|
55
57
|
{ layout: 'masonry', columns: '3', class: { list: 'md:columns-3' } },
|
|
@@ -77,29 +79,34 @@ interface Props {
|
|
|
77
79
|
/** Name of the scrolling row's region when there is no heading ("Images"). */
|
|
78
80
|
regionLabel?: string | undefined
|
|
79
81
|
enlargeLabel?: string | undefined
|
|
80
|
-
/** What a click on an image does: show it large in place, open the file, or nothing. */
|
|
81
|
-
open?: 'popover' | 'link' | 'none' | undefined
|
|
82
|
+
/** What a click on an image does: show it large in place, step through the set in a viewer, open the file, or nothing. */
|
|
83
|
+
open?: 'popover' | 'link' | 'lightbox' | 'none' | undefined
|
|
82
84
|
/** Text of the viewer's close button ("Close"). */
|
|
83
85
|
closeLabel?: string | undefined
|
|
86
|
+
/** The lightbox viewer's buttons ("Previous image", "Next image") and the word of its counter ("2 of 5"). */
|
|
87
|
+
prevLabel?: string | undefined
|
|
88
|
+
nextLabel?: string | undefined
|
|
89
|
+
ofLabel?: string | undefined
|
|
84
90
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
85
91
|
/** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
|
|
86
92
|
measured?: MeasuredStyle | undefined
|
|
87
93
|
id?: string | undefined
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
const { items: given, heading, layout, columns, aspect, gap, captionStyle, hoverEffect, enlargeLabel = 'Enlarge', regionLabel = 'Images', open = 'popover', closeLabel = 'Close', tone, measured, id: givenId } = Astro.props
|
|
96
|
+
const { items: given, heading, layout, columns, aspect, gap, captionStyle, hoverEffect, enlargeLabel = 'Enlarge', regionLabel = 'Images', open = 'popover', closeLabel = 'Close', prevLabel = 'Previous image', nextLabel = 'Next image', ofLabel = 'of', tone, measured, id: givenId } = Astro.props
|
|
91
97
|
// Two on one page never share an id: their labels and controls would point into each other.
|
|
92
98
|
const id = instanceId(Astro.locals, givenId, 'gallery')
|
|
93
99
|
const items = given.flatMap(item => { const image = imageOf(item.image, item.imageAlt); return image ? [{ ...item, image, full: imageOf(item.full, item.imageAlt) }] : [] }) satisfies Array<{ image: ImageInput, caption?: string | undefined }>
|
|
94
100
|
const s = gallery({ layout, columns, open, aspect, gap, captionStyle, hoverEffect })
|
|
95
101
|
const headingId = heading ? `${id}-title` : undefined
|
|
102
|
+
const lightbox = open === 'lightbox' ? `${id}-viewer` : undefined
|
|
96
103
|
const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 25vw, 50vw' : '(min-width: 768px) 33vw, 50vw'
|
|
97
104
|
---
|
|
98
105
|
{items.length > 0 && (
|
|
99
106
|
<Section kit="gallery" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
100
107
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
101
108
|
{/* A scrolling strip is a labelled, focusable region so the keyboard can scroll it. */}
|
|
102
|
-
<div tabindex={layout === 'row' ? 0 : undefined} role={layout === 'row' ? 'region' : undefined} aria-label={layout === 'row' ? heading ?? regionLabel : undefined}>
|
|
109
|
+
<div tabindex={layout === 'row' ? 0 : undefined} role={layout === 'row' ? 'region' : undefined} aria-label={layout === 'row' ? heading ?? regionLabel : undefined} data-kit-lightbox={lightbox}>
|
|
103
110
|
<ul class={s.list({ class: layout === 'grid' || layout === 'mosaic' ? measuredGrid(measured) : undefined })} role="list">
|
|
104
111
|
{items.map((item, i) => {
|
|
105
112
|
const viewer = `${id}-image-${i + 1}`
|
|
@@ -108,9 +115,11 @@ const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 2
|
|
|
108
115
|
<figure class={captionStyle === 'overlay' ? 'group relative m-0 overflow-hidden' : 'm-0'}>
|
|
109
116
|
{open === 'popover'
|
|
110
117
|
? <button type="button" popovertarget={viewer} class={s.frame()} aria-label={`${enlargeLabel}: ${item.image.alt || item.caption || ''}`.trim()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></button>
|
|
111
|
-
: open === '
|
|
112
|
-
? <a href={safeHref(item.image.src)} class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
113
|
-
:
|
|
118
|
+
: open === 'lightbox'
|
|
119
|
+
? <a href={safeHref((item.full ?? item.image).src)} class={s.frame()} aria-label={`${enlargeLabel}: ${item.image.alt || item.caption || ''}`.trim()} data-kit-lightbox-item data-caption={item.caption || undefined}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
120
|
+
: open === 'link'
|
|
121
|
+
? <a href={safeHref(item.image.src)} class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
122
|
+
: <div class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></div>}
|
|
114
123
|
{item.caption && <figcaption class={s.caption()}>{item.caption}</figcaption>}
|
|
115
124
|
</figure>
|
|
116
125
|
{open === 'popover' && (
|
|
@@ -126,6 +135,7 @@ const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 2
|
|
|
126
135
|
)
|
|
127
136
|
})}
|
|
128
137
|
</ul>
|
|
138
|
+
{lightbox && <GalleryLightbox id={lightbox} label={heading ?? regionLabel} closeLabel={closeLabel} prevLabel={prevLabel} nextLabel={nextLabel} ofLabel={ofLabel} single={items.length < 2} />}
|
|
129
139
|
</div>
|
|
130
140
|
</Section>
|
|
131
141
|
)}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The viewer of a Gallery set to `open: 'lightbox'`: one modal dialog for the whole set, printed (with its script) only
|
|
3
|
+
// for that option, so a gallery that does not ask for it ships neither. Each tile is a link to its large file
|
|
4
|
+
// (`data-kit-lightbox-item`); a plain click opens that file here instead, and previous / next step through the set.
|
|
5
|
+
//
|
|
6
|
+
// The dialog is modal (`showModal`): the page behind it is inert, so focus stays inside it, and Escape closes it.
|
|
7
|
+
// Closing hands focus back to the tile it was opened from. ←/→ step (mirrored right to left), Home and End jump to
|
|
8
|
+
// the ends, and a click outside the image closes. The counter and the caption are announced as they change. Nothing
|
|
9
|
+
// animates. Text goes in through `textContent` only. With no script the tiles stay links to the files.
|
|
10
|
+
interface Props {
|
|
11
|
+
/** The dialog's id; the gallery names it in `data-kit-lightbox`. */
|
|
12
|
+
id: string
|
|
13
|
+
/** The dialog's name: the gallery's heading, or its region label. */
|
|
14
|
+
label: string
|
|
15
|
+
closeLabel: string
|
|
16
|
+
prevLabel: string
|
|
17
|
+
nextLabel: string
|
|
18
|
+
/** The counter's word between position and total ("2 of 5"). */
|
|
19
|
+
ofLabel: string
|
|
20
|
+
/** One image: no previous / next buttons and no counter. */
|
|
21
|
+
single: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { id, label, closeLabel, prevLabel, nextLabel, ofLabel, single } = Astro.props
|
|
25
|
+
const step = 'grid size-11 shrink-0 cursor-pointer place-items-center rounded-full border border-surface/40 text-surface hover:bg-surface/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface'
|
|
26
|
+
---
|
|
27
|
+
<dialog id={id} aria-label={label} data-of={ofLabel} class="m-0 h-dvh max-h-none w-screen max-w-none border-0 bg-ink p-4 text-surface open:grid open:grid-rows-[auto_minmax(0,1fr)_auto] open:gap-3 md:p-8">
|
|
28
|
+
<div class="flex justify-end">
|
|
29
|
+
<button type="button" class="cursor-pointer rounded-md border border-surface/40 px-4 py-2 text-sm text-surface hover:bg-surface/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface" data-kit-lightbox-close>{closeLabel}</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="grid min-h-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 md:gap-4">
|
|
32
|
+
{single ? <span /> : <button type="button" class={step} aria-label={prevLabel} data-kit-lightbox-prev><svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 rtl:-scale-x-100" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6" /></svg></button>}
|
|
33
|
+
<img alt="" class="mx-auto block max-h-full max-w-full rounded-[var(--radius-image,var(--radius-card))] object-contain" data-kit-lightbox-image hidden />
|
|
34
|
+
{single ? <span /> : <button type="button" class={step} aria-label={nextLabel} data-kit-lightbox-next><svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 rtl:-scale-x-100" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" /></svg></button>}
|
|
35
|
+
</div>
|
|
36
|
+
<p class="m-0 flex flex-wrap justify-center gap-x-3 text-center text-sm" aria-live="polite">
|
|
37
|
+
<span data-kit-lightbox-caption></span>
|
|
38
|
+
{!single && <span data-kit-lightbox-count></span>}
|
|
39
|
+
</p>
|
|
40
|
+
</dialog>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
function wire(root: HTMLElement): void {
|
|
44
|
+
const dialog = document.getElementById(root.dataset.kitLightbox ?? '')
|
|
45
|
+
if (!(dialog instanceof HTMLDialogElement)) return
|
|
46
|
+
// Only tiles with a real address: one the kit printed as `#` (a refused URL) stays a plain link.
|
|
47
|
+
const links = [...root.querySelectorAll<HTMLAnchorElement>('a[data-kit-lightbox-item]')].filter(link => link.getAttribute('href') !== '#')
|
|
48
|
+
const image = dialog.querySelector<HTMLImageElement>('[data-kit-lightbox-image]')
|
|
49
|
+
const caption = dialog.querySelector<HTMLElement>('[data-kit-lightbox-caption]')
|
|
50
|
+
const count = dialog.querySelector<HTMLElement>('[data-kit-lightbox-count]')
|
|
51
|
+
if (!links.length || !image || !caption) return
|
|
52
|
+
// One tile left once the refused ones are out: nothing to step to, so no step buttons and no counter. The buttons
|
|
53
|
+
// keep their place in the row (hidden from sight, focus and readers alike), so the image does not move.
|
|
54
|
+
if (links.length < 2) {
|
|
55
|
+
for (const button of dialog.querySelectorAll<HTMLElement>('[data-kit-lightbox-prev], [data-kit-lightbox-next]')) button.style.visibility = 'hidden'
|
|
56
|
+
if (count) count.hidden = true
|
|
57
|
+
}
|
|
58
|
+
let index = 0
|
|
59
|
+
let opener: HTMLAnchorElement | null = null
|
|
60
|
+
const show = (at: number) => {
|
|
61
|
+
index = (at + links.length) % links.length
|
|
62
|
+
const link = links[index]!
|
|
63
|
+
image.src = link.href
|
|
64
|
+
image.alt = link.querySelector('img')?.alt ?? ''
|
|
65
|
+
image.hidden = false
|
|
66
|
+
caption.textContent = link.dataset.caption ?? ''
|
|
67
|
+
if (count) count.textContent = `${index + 1} ${dialog.dataset.of ?? 'of'} ${links.length}`
|
|
68
|
+
}
|
|
69
|
+
links.forEach((link, i) => link.addEventListener('click', (event) => {
|
|
70
|
+
// A click asking for a new tab or window, or a download, keeps the link's own meaning.
|
|
71
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
opener = link
|
|
74
|
+
show(i)
|
|
75
|
+
if (!dialog.open) dialog.showModal()
|
|
76
|
+
}))
|
|
77
|
+
dialog.querySelector('[data-kit-lightbox-close]')?.addEventListener('click', () => dialog.close())
|
|
78
|
+
dialog.querySelector('[data-kit-lightbox-prev]')?.addEventListener('click', () => show(index - 1))
|
|
79
|
+
dialog.querySelector('[data-kit-lightbox-next]')?.addEventListener('click', () => show(index + 1))
|
|
80
|
+
dialog.addEventListener('keydown', (event) => {
|
|
81
|
+
const back = getComputedStyle(dialog).direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft'
|
|
82
|
+
const forward = back === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft'
|
|
83
|
+
const to = event.key === back ? index - 1 : event.key === forward ? index + 1 : event.key === 'Home' ? 0 : event.key === 'End' ? links.length - 1 : null
|
|
84
|
+
if (to === null || links.length < 2) return
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
show(to)
|
|
87
|
+
})
|
|
88
|
+
// The dialog fills the screen: a click anywhere but on the image or a button is a click outside the image.
|
|
89
|
+
dialog.addEventListener('click', (event) => {
|
|
90
|
+
if (event.target instanceof Element && (event.target === image || event.target.closest('button'))) return
|
|
91
|
+
dialog.close()
|
|
92
|
+
})
|
|
93
|
+
dialog.addEventListener('close', () => { opener?.focus(); opener = null })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function init(): void {
|
|
97
|
+
for (const root of document.querySelectorAll<HTMLElement>('[data-kit-lightbox]')) {
|
|
98
|
+
if (root.dataset.kitReady !== undefined) continue
|
|
99
|
+
root.dataset.kitReady = ''
|
|
100
|
+
try { wire(root) }
|
|
101
|
+
catch {
|
|
102
|
+
// Left as printed: every tile is still a link to its file.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
init()
|
|
108
|
+
// With Astro's client router a page arrives without a reload: wire its galleries too (ready ones are skipped).
|
|
109
|
+
document.addEventListener('astro:page-load', init)
|
|
110
|
+
</script>
|
|
@@ -462,5 +462,57 @@
|
|
|
462
462
|
}
|
|
463
463
|
]
|
|
464
464
|
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"name": "lightbox",
|
|
468
|
+
"props": {
|
|
469
|
+
"heading": "Along the coast",
|
|
470
|
+
"id": "coast",
|
|
471
|
+
"open": "lightbox",
|
|
472
|
+
"columns": "4",
|
|
473
|
+
"items": [
|
|
474
|
+
{
|
|
475
|
+
"image": {
|
|
476
|
+
"src": "/kit-fixtures/photo-1.svg",
|
|
477
|
+
"alt": "Photo 1",
|
|
478
|
+
"width": 1200,
|
|
479
|
+
"height": 800
|
|
480
|
+
},
|
|
481
|
+
"full": {
|
|
482
|
+
"src": "/kit-fixtures/photo-1.svg",
|
|
483
|
+
"alt": "Photo 1",
|
|
484
|
+
"width": 1200,
|
|
485
|
+
"height": 800
|
|
486
|
+
},
|
|
487
|
+
"caption": "Porto at dawn"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"image": {
|
|
491
|
+
"src": "/kit-fixtures/photo-2.svg",
|
|
492
|
+
"alt": "Photo 2",
|
|
493
|
+
"width": 1200,
|
|
494
|
+
"height": 800
|
|
495
|
+
},
|
|
496
|
+
"caption": "Train to Coimbra"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"image": {
|
|
500
|
+
"src": "/kit-fixtures/photo-3.svg",
|
|
501
|
+
"alt": "Photo 3",
|
|
502
|
+
"width": 1200,
|
|
503
|
+
"height": 800
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"image": {
|
|
508
|
+
"src": "/kit-fixtures/photo-4.svg",
|
|
509
|
+
"alt": "Photo 4",
|
|
510
|
+
"width": 1200,
|
|
511
|
+
"height": 800
|
|
512
|
+
},
|
|
513
|
+
"caption": "Harbour"
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
}
|
|
465
517
|
}
|
|
466
518
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "gallery",
|
|
3
3
|
"name": "Gallery",
|
|
4
|
-
"description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), opens the image file, or does nothing.",
|
|
4
|
+
"description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), steps through the set in a lightbox viewer, opens the image file, or does nothing.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": [
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"options": [
|
|
41
41
|
"popover",
|
|
42
42
|
"link",
|
|
43
|
-
"none"
|
|
43
|
+
"none",
|
|
44
|
+
"lightbox"
|
|
44
45
|
],
|
|
45
46
|
"default": "popover",
|
|
46
|
-
"description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only."
|
|
47
|
+
"description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only. lightbox: each thumbnail links to its large file (full, else the image); with script a click opens one modal viewer for the whole set, with previous / next, ←/→, a counter and the caption (a source lightbox: Elementor's slideshow group, a WordPress gallery with a lightbox plugin). The viewer and its script are printed only for this option."
|
|
47
48
|
},
|
|
48
49
|
"aspect": {
|
|
49
50
|
"options": [
|
|
@@ -124,6 +125,24 @@
|
|
|
124
125
|
"content": false,
|
|
125
126
|
"description": "Text of the viewer's close button (ui-strings gallery.close)."
|
|
126
127
|
},
|
|
128
|
+
"prevLabel": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"default": "Previous image",
|
|
131
|
+
"content": false,
|
|
132
|
+
"description": "Name of the lightbox viewer's previous button (ui-strings slider.prev)."
|
|
133
|
+
},
|
|
134
|
+
"nextLabel": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"default": "Next image",
|
|
137
|
+
"content": false,
|
|
138
|
+
"description": "Name of the lightbox viewer's next button (ui-strings slider.next)."
|
|
139
|
+
},
|
|
140
|
+
"ofLabel": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"default": "of",
|
|
143
|
+
"content": false,
|
|
144
|
+
"description": "The word of the lightbox viewer's counter between position and total (\"2 of 5\"; ui-strings slider.of)."
|
|
145
|
+
},
|
|
127
146
|
"tone": {
|
|
128
147
|
"type": "select",
|
|
129
148
|
"options": [
|
|
@@ -264,7 +283,7 @@
|
|
|
264
283
|
}
|
|
265
284
|
},
|
|
266
285
|
"slots": {},
|
|
267
|
-
"js": "
|
|
286
|
+
"js": "vanilla",
|
|
268
287
|
"dependencies": {},
|
|
269
288
|
"sources": {
|
|
270
289
|
"gutenberg": [
|
|
@@ -281,5 +300,5 @@
|
|
|
281
300
|
"classic/gallery"
|
|
282
301
|
]
|
|
283
302
|
},
|
|
284
|
-
"a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s. A scrolling row is a labelled region with tabindex 0 so the keyboard can scroll it; thumbnail buttons are named \"Enlarge: …\". The viewer
|
|
303
|
+
"a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s. A scrolling row is a labelled region with tabindex 0 so the keyboard can scroll it; thumbnail buttons are named \"Enlarge: …\". The popover viewer shows one image. In lightbox mode each thumbnail is a link to its large file, named \"Enlarge: …\"; with script it opens one modal <dialog> named by the heading (or the region label): focus moves into it and stays there (the page behind is inert), Escape or a click outside the image closes it and focus returns to the thumbnail, labelled previous / next buttons and ←/→ (mirrored right to left), Home and End step through the set, and the counter (\"2 of 5\") and caption are announced as they change. Nothing animates. Without script the links open the files. Other modes ship no script."
|
|
285
304
|
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// out by more than its colour. Each plan's features are a markdown list,
|
|
5
5
|
// rendered at build time. Variants: `layout="table"` is a comparison table
|
|
6
6
|
// (plan columns, feature rows); `billingToggle="radio"` swaps monthly and yearly
|
|
7
|
-
// prices with two radio inputs and CSS `:has()` — no script.
|
|
7
|
+
// prices with two radio inputs and CSS `:has()` — no script. A plan's struck price, its note under the price and its
|
|
8
|
+
// line by the button swap the same way, and the section's `billingAltNote` shows only with the second option; each
|
|
9
|
+
// is printed only where it is given. An Alt line never falls back to the main one: with the second option chosen, a
|
|
10
|
+
// plan without it shows no line there (a note "for the first month" would be wrong under a yearly price).
|
|
11
|
+
// The price itself is the one exception: a plan with no `priceAlt` (free, "Contact us", or one price for both periods)
|
|
12
|
+
// keeps its price line, struck price included, under both options; a card never loses its price.
|
|
8
13
|
import { tv } from 'tailwind-variants'
|
|
9
14
|
import Button from '../_shared/Button.astro'
|
|
10
15
|
import Section, { type Tone } from '../_shared/Section.astro'
|
|
@@ -32,6 +37,10 @@ const pricing = tv({
|
|
|
32
37
|
price: 'flex items-baseline gap-1',
|
|
33
38
|
amount: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight',
|
|
34
39
|
period: `text-sm ${muted}`,
|
|
40
|
+
was: `me-1 text-lg ${muted}`,
|
|
41
|
+
note: `-mt-2 text-sm ${muted}`,
|
|
42
|
+
foot: `text-sm ${muted}`,
|
|
43
|
+
altNote: 'mx-auto -mt-5 mb-8 w-fit text-center text-sm font-medium text-accent-text',
|
|
35
44
|
description: `text-pretty ${muted}`,
|
|
36
45
|
features: 'prose-site prose max-w-none text-current prose-ul:ps-5 prose-li:my-1',
|
|
37
46
|
action: 'mt-auto pt-2',
|
|
@@ -69,6 +78,15 @@ interface Plan {
|
|
|
69
78
|
/** The other billing period's price and period (`billingToggle="radio"`). */
|
|
70
79
|
priceAlt?: string | undefined
|
|
71
80
|
periodAlt?: string | undefined
|
|
81
|
+
/** The struck original price before the price, for each period ("$35"). */
|
|
82
|
+
priceWas?: string | undefined
|
|
83
|
+
priceWasAlt?: string | undefined
|
|
84
|
+
/** One line under the price, for each period ("USD for the first month"). */
|
|
85
|
+
priceNote?: string | undefined
|
|
86
|
+
priceNoteAlt?: string | undefined
|
|
87
|
+
/** One line at the card's foot, by the button, for each period ("Pay $0 today, $35 after the first month"). */
|
|
88
|
+
priceFoot?: string | undefined
|
|
89
|
+
priceFootAlt?: string | undefined
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
interface Props {
|
|
@@ -88,6 +106,10 @@ interface Props {
|
|
|
88
106
|
billingMainLabel?: string | undefined
|
|
89
107
|
billingAltLabel?: string | undefined
|
|
90
108
|
billingLegend?: string | undefined
|
|
109
|
+
/** A line by the toggle shown only with its second option ("Save $70 by paying annually"). */
|
|
110
|
+
billingAltNote?: string | undefined
|
|
111
|
+
/** Read before a struck price by screen readers ("Was"), which do not announce the strike. */
|
|
112
|
+
wasLabel?: string | undefined
|
|
91
113
|
/** Table cell texts for screen readers. */
|
|
92
114
|
includedLabel?: string | undefined
|
|
93
115
|
notIncludedLabel?: string | undefined
|
|
@@ -97,7 +119,7 @@ interface Props {
|
|
|
97
119
|
id?: string | undefined
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
const { heading, intro, plans, featuredLabel = 'Most popular', columns, layout = 'cards', featureStyle, compact, billingToggle = 'none', billingMainLabel = 'Monthly', billingAltLabel = 'Yearly', billingLegend = 'Billing period', includedLabel = 'Included', notIncludedLabel = 'Not included', tone, measured, id: givenId } = Astro.props
|
|
122
|
+
const { heading, intro, plans, featuredLabel = 'Most popular', columns, layout = 'cards', featureStyle, compact, billingToggle = 'none', billingMainLabel = 'Monthly', billingAltLabel = 'Yearly', billingLegend = 'Billing period', billingAltNote, wasLabel = 'Was', includedLabel = 'Included', notIncludedLabel = 'Not included', tone, measured, id: givenId } = Astro.props
|
|
101
123
|
// Every section has an id, so a theme or a design layer can address this one: the page's, or `pricing`, `pricing-2`, … in render order.
|
|
102
124
|
const id = instanceId(Astro.locals, givenId, 'pricing')
|
|
103
125
|
const s = pricing({ columns, featureStyle, compact: compact ?? false, tone })
|
|
@@ -173,28 +195,41 @@ const features = await Promise.all(plans.map(plan => renderMarkdown(plan.feature
|
|
|
173
195
|
<label class={s.toggleOption()}><input type="radio" name={`${group}-billing`} value="alt" class="sr-only" />{billingAltLabel}</label>
|
|
174
196
|
</fieldset>
|
|
175
197
|
)}
|
|
198
|
+
{toggled && billingAltNote && <p class={s.altNote()} data-price="alt">{billingAltNote}</p>}
|
|
176
199
|
<ul class={s.list({ class: measuredGrid(measured) })}>
|
|
177
200
|
{plans.map((plan, i) => (
|
|
178
201
|
<li class={s.plan({ class: plan.featured ? 'ring-2 ring-accent shadow-lg md:-my-2 md:py-10' : undefined })}>
|
|
179
202
|
{(plan.badge ?? (plan.featured ? featuredLabel : undefined)) && <p class={s.badge()}>{plan.badge ?? featuredLabel}</p>}
|
|
180
203
|
<PlanHeading class={s.name()}>{plan.name}</PlanHeading>
|
|
181
|
-
<p class={s.price()} data-price=
|
|
204
|
+
<p class={s.price()} data-price={toggled && !plan.priceAlt ? undefined : 'main'}>
|
|
205
|
+
{plan.priceWas && <del class={s.was()}><span class="sr-only">{wasLabel} </span>{plan.priceWas}</del>}
|
|
182
206
|
<span class={s.amount()}>{plan.price}</span>
|
|
183
207
|
{plan.period && <span class={s.period()}>{plan.period}</span>}
|
|
184
208
|
</p>
|
|
185
209
|
{toggled && plan.priceAlt && (
|
|
186
210
|
<p class={s.price()} data-price="alt">
|
|
211
|
+
{plan.priceWasAlt && <del class={s.was()}><span class="sr-only">{wasLabel} </span>{plan.priceWasAlt}</del>}
|
|
187
212
|
<span class={s.amount()}>{plan.priceAlt}</span>
|
|
188
213
|
{plan.periodAlt && <span class={s.period()}>{plan.periodAlt}</span>}
|
|
189
214
|
</p>
|
|
190
215
|
)}
|
|
216
|
+
{plan.priceNote && <p class={s.note()} data-price="main">{plan.priceNote}</p>}
|
|
217
|
+
{toggled && plan.priceAlt && plan.priceNoteAlt && <p class={s.note()} data-price="alt">{plan.priceNoteAlt}</p>}
|
|
191
218
|
{plan.description && <p class={s.description()} data-kit-text>{plan.description}</p>}
|
|
192
219
|
{features[i] && <div class={s.features()} set:html={features[i]} />}
|
|
193
220
|
{plan.ctaLabel && plan.ctaHref && (
|
|
194
221
|
<div class={s.action()}>
|
|
222
|
+
{plan.priceFoot && <p class={s.foot({ class: 'mb-3' })} data-price="main">{plan.priceFoot}</p>}
|
|
223
|
+
{toggled && plan.priceAlt && plan.priceFootAlt && <p class={s.foot({ class: 'mb-3' })} data-price="alt">{plan.priceFootAlt}</p>}
|
|
195
224
|
<Button action={{ label: plan.ctaLabel, href: plan.ctaHref, style: plan.featured ? 'primary' : 'ghost' }} class="w-full" />
|
|
196
225
|
</div>
|
|
197
226
|
)}
|
|
227
|
+
{!(plan.ctaLabel && plan.ctaHref) && (plan.priceFoot || (toggled && plan.priceAlt && plan.priceFootAlt)) && (
|
|
228
|
+
<div class={s.action()}>
|
|
229
|
+
{plan.priceFoot && <p class={s.foot()} data-price="main">{plan.priceFoot}</p>}
|
|
230
|
+
{toggled && plan.priceAlt && plan.priceFootAlt && <p class={s.foot()} data-price="alt">{plan.priceFootAlt}</p>}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
198
233
|
</li>
|
|
199
234
|
))}
|
|
200
235
|
</ul>
|
|
@@ -286,5 +286,89 @@
|
|
|
286
286
|
}
|
|
287
287
|
]
|
|
288
288
|
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "billing-period-notes",
|
|
292
|
+
"props": {
|
|
293
|
+
"id": "hosting",
|
|
294
|
+
"heading": "Hosting",
|
|
295
|
+
"billingToggle": "radio",
|
|
296
|
+
"columns": "2",
|
|
297
|
+
"billingAltNote": "Save $70 by paying annually",
|
|
298
|
+
"plans": [
|
|
299
|
+
{
|
|
300
|
+
"name": "Single",
|
|
301
|
+
"price": "$0",
|
|
302
|
+
"period": "/first month",
|
|
303
|
+
"priceWas": "$35",
|
|
304
|
+
"priceNote": "USD for the first month",
|
|
305
|
+
"priceFoot": "Pay $0 today, $35 after the first month",
|
|
306
|
+
"priceAlt": "$0",
|
|
307
|
+
"periodAlt": "/first month",
|
|
308
|
+
"priceWasAlt": "$30",
|
|
309
|
+
"priceNoteAlt": "USD for the first month, then $350 a year",
|
|
310
|
+
"priceFootAlt": "Pay $0 today, $350 after the first month",
|
|
311
|
+
"features": "- 1 site\n- 20GB bandwidth",
|
|
312
|
+
"ctaLabel": "Sign up",
|
|
313
|
+
"ctaHref": "/signup/single/"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "Multiple",
|
|
317
|
+
"price": "$0",
|
|
318
|
+
"period": "/first month",
|
|
319
|
+
"priceWas": "$70",
|
|
320
|
+
"priceNote": "USD for the first month",
|
|
321
|
+
"priceFoot": "Pay $0 today, $70 after the first month",
|
|
322
|
+
"priceAlt": "$0",
|
|
323
|
+
"periodAlt": "/first month",
|
|
324
|
+
"priceWasAlt": "$58",
|
|
325
|
+
"priceFootAlt": "Pay $0 today, $700 after the first month",
|
|
326
|
+
"features": "- 5 sites\n- 100GB bandwidth",
|
|
327
|
+
"featured": true
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"name": "billing-toggle-no-alt",
|
|
334
|
+
"props": {
|
|
335
|
+
"id": "teams",
|
|
336
|
+
"heading": "Teams",
|
|
337
|
+
"billingToggle": "radio",
|
|
338
|
+
"columns": "3",
|
|
339
|
+
"plans": [
|
|
340
|
+
{
|
|
341
|
+
"name": "Free",
|
|
342
|
+
"price": "$0",
|
|
343
|
+
"period": "/month",
|
|
344
|
+
"priceNote": "Free for one user",
|
|
345
|
+
"features": "- 1 user",
|
|
346
|
+
"ctaLabel": "Start free",
|
|
347
|
+
"ctaHref": "/signup/free/"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "Team",
|
|
351
|
+
"price": "$12",
|
|
352
|
+
"period": "/month",
|
|
353
|
+
"priceAlt": "$120",
|
|
354
|
+
"periodAlt": "/year",
|
|
355
|
+
"features": "- 10 users\n- Shared drafts",
|
|
356
|
+
"ctaLabel": "Subscribe",
|
|
357
|
+
"ctaHref": "/buy/team/",
|
|
358
|
+
"featured": true
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"name": "Enterprise",
|
|
362
|
+
"price": "Contact us",
|
|
363
|
+
"priceWas": "$99",
|
|
364
|
+
"features": "- Unlimited users\n- SSO",
|
|
365
|
+
"ctaLabel": "Talk to sales",
|
|
366
|
+
"ctaHref": "/contact/"
|
|
367
|
+
}
|
|
368
|
+
],
|
|
369
|
+
"billingMainLabel": "Monthly",
|
|
370
|
+
"billingAltLabel": "Yearly",
|
|
371
|
+
"billingLegend": "Billing period"
|
|
372
|
+
}
|
|
289
373
|
}
|
|
290
374
|
]
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"radio"
|
|
57
57
|
],
|
|
58
58
|
"default": "none",
|
|
59
|
-
"description": "radio: two labelled radios swap each plan's price for its priceAlt (monthly / yearly), with no script. The radio group is named from the section id, or a hash of the plans without one, so two toggles on a page stay independent."
|
|
59
|
+
"description": "radio: two labelled radios swap each plan's price for its priceAlt (monthly / yearly), with no script. The radio group is named from the section id, or a hash of the plans without one, so two toggles on a page stay independent. A plan's priceWas, priceNote and priceFoot swap for their Alt values the same way, and billingAltNote shows only with the second option."
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"props": {
|
|
@@ -127,12 +127,42 @@
|
|
|
127
127
|
"priceAlt": {
|
|
128
128
|
"type": "string",
|
|
129
129
|
"label": "Price, other period",
|
|
130
|
-
"description": "The price for the other billing period (\"€150\"), shown when the toggle is on its second option."
|
|
130
|
+
"description": "The price for the other billing period (\"€150\"), shown when the toggle is on its second option. Without it the plan's price (and its struck price) shows under both options: a free plan, \"Contact us\", or one price for both periods. Its notes do not: they stay first-period lines."
|
|
131
131
|
},
|
|
132
132
|
"periodAlt": {
|
|
133
133
|
"type": "string",
|
|
134
134
|
"label": "Period, other period",
|
|
135
135
|
"description": "What the other price is per (\"/year\")."
|
|
136
|
+
},
|
|
137
|
+
"priceWas": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"label": "Original price",
|
|
140
|
+
"description": "The original price, struck before the price (\"$35\"; cards layout)."
|
|
141
|
+
},
|
|
142
|
+
"priceWasAlt": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"label": "Original price, other period",
|
|
145
|
+
"description": "The other period's struck original price, shown with priceAlt. No fallback: without it, the second option shows no struck price (priceWas belongs to the first period only, except on a plan with no priceAlt, whose whole price line shows under both)."
|
|
146
|
+
},
|
|
147
|
+
"priceNote": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"label": "Price note",
|
|
150
|
+
"description": "One line under the price (\"USD for the first month\"; cards layout)."
|
|
151
|
+
},
|
|
152
|
+
"priceNoteAlt": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"label": "Price note, other period",
|
|
155
|
+
"description": "The other period's line under the price, shown with priceAlt. No fallback: without it, the second option shows no line there (a note like \"for the first month\" would be wrong under the other price)."
|
|
156
|
+
},
|
|
157
|
+
"priceFoot": {
|
|
158
|
+
"type": "string",
|
|
159
|
+
"label": "Footnote",
|
|
160
|
+
"description": "One line at the card's foot, above the button (\"Pay $0 today, $35 after the first month\"; cards layout)."
|
|
161
|
+
},
|
|
162
|
+
"priceFootAlt": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"label": "Footnote, other period",
|
|
165
|
+
"description": "The other period's line at the card's foot, shown with priceAlt. No fallback: without it, the second option shows no line there."
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
168
|
},
|
|
@@ -142,7 +172,7 @@
|
|
|
142
172
|
"type": "string",
|
|
143
173
|
"default": "Most popular",
|
|
144
174
|
"content": false,
|
|
145
|
-
"description": "Badge text on featured plans."
|
|
175
|
+
"description": "Badge text on featured plans (ui-strings pricing.featured)."
|
|
146
176
|
},
|
|
147
177
|
"id": {
|
|
148
178
|
"type": "string",
|
|
@@ -159,13 +189,13 @@
|
|
|
159
189
|
"type": "string",
|
|
160
190
|
"default": "Included",
|
|
161
191
|
"content": false,
|
|
162
|
-
"description": "Table cell text for readers where a plan has the feature."
|
|
192
|
+
"description": "Table cell text for readers where a plan has the feature (ui-strings pricing.included)."
|
|
163
193
|
},
|
|
164
194
|
"notIncludedLabel": {
|
|
165
195
|
"type": "string",
|
|
166
196
|
"default": "Not included",
|
|
167
197
|
"content": false,
|
|
168
|
-
"description": "Table cell text for readers where a plan lacks the feature."
|
|
198
|
+
"description": "Table cell text for readers where a plan lacks the feature (ui-strings pricing.not_included)."
|
|
169
199
|
},
|
|
170
200
|
"measured": {
|
|
171
201
|
"type": "object",
|
|
@@ -281,19 +311,30 @@
|
|
|
281
311
|
"type": "string",
|
|
282
312
|
"default": "Monthly",
|
|
283
313
|
"content": false,
|
|
284
|
-
"description": "First option of the billing toggle."
|
|
314
|
+
"description": "First option of the billing toggle (ui-strings pricing.billing_main)."
|
|
285
315
|
},
|
|
286
316
|
"billingAltLabel": {
|
|
287
317
|
"type": "string",
|
|
288
318
|
"default": "Yearly",
|
|
289
319
|
"content": false,
|
|
290
|
-
"description": "Second option of the billing toggle
|
|
320
|
+
"description": "Second option of the billing toggle, the plans' priceAlt (ui-strings pricing.billing_alt)."
|
|
291
321
|
},
|
|
292
322
|
"billingLegend": {
|
|
293
323
|
"type": "string",
|
|
294
324
|
"default": "Billing period",
|
|
295
325
|
"content": false,
|
|
296
|
-
"description": "Legend of the toggle, read to screen readers."
|
|
326
|
+
"description": "Legend of the toggle, read to screen readers (ui-strings pricing.billing_legend)."
|
|
327
|
+
},
|
|
328
|
+
"billingAltNote": {
|
|
329
|
+
"type": "string",
|
|
330
|
+
"label": "Second-option note",
|
|
331
|
+
"description": "A line under the billing toggle shown only while its second option is chosen (\"Save $70 by paying annually\")."
|
|
332
|
+
},
|
|
333
|
+
"wasLabel": {
|
|
334
|
+
"type": "string",
|
|
335
|
+
"default": "Was",
|
|
336
|
+
"content": false,
|
|
337
|
+
"description": "Read before a struck original price by screen readers, which do not announce the strike. 'Was' is only the kit default: the writer passes it in the site's language (ui-strings pricing.was)."
|
|
297
338
|
}
|
|
298
339
|
},
|
|
299
340
|
"slots": {},
|
|
@@ -310,7 +351,7 @@
|
|
|
310
351
|
"divi/et_pb_pricing_tables"
|
|
311
352
|
]
|
|
312
353
|
},
|
|
313
|
-
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label. The table layout is a real <table> with a caption, column and row headers, and Included / Not included text in every cell; the billing toggle is a fieldset with a legend and labelled radios.",
|
|
354
|
+
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label. The table layout is a real <table> with a caption, column and row headers, and Included / Not included text in every cell; the billing toggle is a fieldset with a legend and labelled radios. A struck original price is a <del> read as \"Was <price>\" (the strike alone is not announced); the period's notes swap with its price, hidden (display: none) for the period not chosen.",
|
|
314
355
|
"pattern": {
|
|
315
356
|
"summary": "repeated plan units: name + price + feature list + button",
|
|
316
357
|
"signals": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/astro-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
|
|
6
6
|
"type": "module",
|