@grantcodes/ui 2.6.0 → 2.8.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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/custom-elements.json +556 -0
  3. package/package.json +5 -5
  4. package/src/components/accordion/accordion.component.js +4 -1
  5. package/src/components/accordion/accordion.css +26 -18
  6. package/src/components/code-preview/code-preview.css +5 -0
  7. package/src/components/container/container.css +6 -0
  8. package/src/components/countdown/countdown.component.js +180 -0
  9. package/src/components/countdown/countdown.css +62 -0
  10. package/src/components/countdown/countdown.js +6 -0
  11. package/src/components/countdown/countdown.react.js +9 -0
  12. package/src/components/countdown/countdown.stories.js +65 -0
  13. package/src/components/countdown/index.js +1 -0
  14. package/src/components/cta/cta.css +6 -0
  15. package/src/components/dialog/dialog.css +5 -0
  16. package/src/components/feature-list/feature-list.css +6 -0
  17. package/src/components/footer/footer.css +3 -1
  18. package/src/components/form-field/form-field.css +6 -0
  19. package/src/components/gallery/gallery-image.component.js +2 -0
  20. package/src/components/gallery/gallery.component.js +35 -6
  21. package/src/components/gallery/gallery.css +73 -0
  22. package/src/components/gallery/gallery.stories.js +42 -0
  23. package/src/components/gallery/gallery.test.js +24 -0
  24. package/src/components/hero/hero.component.js +7 -0
  25. package/src/components/hero/hero.css +18 -1
  26. package/src/components/hero/hero.stories.js +30 -0
  27. package/src/components/icon/icon.css +6 -0
  28. package/src/components/loading/loading.css +5 -0
  29. package/src/components/logo-cloud/logo-cloud.css +6 -0
  30. package/src/components/map/index.js +1 -0
  31. package/src/components/map/map.component.js +135 -0
  32. package/src/components/map/map.css +41 -0
  33. package/src/components/map/map.js +6 -0
  34. package/src/components/map/map.react.js +9 -0
  35. package/src/components/map/map.stories.js +68 -0
  36. package/src/components/media-text/media-text.css +6 -0
  37. package/src/components/newsletter/newsletter.css +6 -0
  38. package/src/components/notice/notice.css +5 -0
  39. package/src/components/pagination/pagination.css +5 -0
  40. package/src/components/pricing/pricing.css +6 -0
  41. package/src/components/stats/stats.css +6 -0
  42. package/src/components/testimonials/testimonials.css +6 -0
  43. package/src/components/tooltip/tooltip.css +5 -0
  44. package/src/lib/styles/all.css +2 -0
  45. package/src/main.js +2 -0
  46. package/src/pages/blog-post.stories.js +7 -19
  47. package/src/react.js +2 -0
  48. package/src/types.d.ts +18 -0
@@ -1,3 +1,9 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
1
7
  :host {
2
8
  display: block;
3
9
  }
@@ -9,51 +15,53 @@
9
15
  }
10
16
 
11
17
  .accordion__item {
12
- border: 1px solid var(--g-theme-color-border-default);
18
+ border: 1px solid var(--g-theme-color-border-subtle, var(--g-theme-color-border-default));
13
19
  border-radius: var(--g-theme-border-radius-md, 0.5rem);
14
- height: 3.5rem;
15
- transition: height 0.25s;
16
- }
17
-
18
- .accordion__item[open] {
19
- height: auto;
20
- overflow: clip;
21
20
  }
22
21
 
23
22
  .accordion__summary {
24
23
  padding: var(--g-theme-spacing-md);
25
24
  cursor: pointer;
26
25
  background: var(--g-theme-color-background-subtle);
27
- border: var(--g-theme-border-width-md) solid
28
- var(--g-theme-color-border-subtle);
29
- border-radius: var(--g-theme-border-radius-md);
30
26
  font-weight: var(--g-typography-font-weight-500);
31
27
  list-style: none;
32
28
  display: flex;
33
29
  justify-content: space-between;
34
30
  align-items: center;
31
+ gap: var(--g-theme-spacing-md);
32
+ border-radius: var(--g-theme-border-radius-md, 0.5rem);
35
33
  transition-property: background-color, outline-width, outline-color;
36
34
  }
37
35
 
38
36
  .accordion__summary:hover {
39
- background: var(--g-theme-color-background-subtle-hover);
37
+ background: var(--g-theme-color-background-subtle-hover, var(--g-theme-color-background-subtle));
40
38
  }
41
39
 
42
40
  .accordion__summary::-webkit-details-marker {
43
41
  display: none;
44
42
  }
45
43
 
46
- .accordion__summary::after {
47
- content: "+";
48
- font: var(--g-theme-typography-h5);
49
- line-height: 1;
44
+ .accordion__chevron {
45
+ display: block;
46
+ inline-size: 1em;
47
+ block-size: 1em;
48
+ flex-shrink: 0;
49
+ transition: transform 0.25s ease;
50
+ color: var(--g-theme-color-content-secondary, currentColor);
50
51
  }
51
52
 
52
- .accordion__item[open] .accordion__summary::after {
53
- content: "-";
53
+ .accordion__item[open] .accordion__chevron {
54
+ transform: rotateX(180deg);
54
55
  }
55
56
 
56
57
  .accordion__content {
57
58
  padding: var(--g-theme-spacing-md);
58
59
  background: var(--g-theme-color-background-default);
60
+ border-radius: 0 0 var(--g-theme-border-radius-md, 0.5rem) var(--g-theme-border-radius-md, 0.5rem);
61
+ }
62
+
63
+ @media (prefers-reduced-motion: reduce) {
64
+ .accordion__chevron {
65
+ transition: none;
66
+ }
59
67
  }
@@ -1,3 +1,8 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
1
6
 
2
7
  .code-preview > pre {
3
8
  display: block;
@@ -1,3 +1,9 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
1
7
  .container {
2
8
  display: flex;
3
9
  flex-direction: column;
@@ -0,0 +1,180 @@
1
+ import { LitElement, html, nothing } from "lit";
2
+ import countdownStyles from "./countdown.css" with { type: "css" };
3
+
4
+ export class GrantCodesCountdown extends LitElement {
5
+ static styles = [countdownStyles];
6
+
7
+ static properties = {
8
+ /**
9
+ * ISO 8601 datetime string for the countdown target.
10
+ * @type {string}
11
+ */
12
+ target: { type: String },
13
+ /**
14
+ * Label for the days unit.
15
+ * @type {string}
16
+ */
17
+ "days-label": { type: String, attribute: "days-label" },
18
+ /**
19
+ * Label for the hours unit.
20
+ * @type {string}
21
+ */
22
+ "hours-label": { type: String, attribute: "hours-label" },
23
+ /**
24
+ * Label for the minutes unit.
25
+ * @type {string}
26
+ */
27
+ "minutes-label": { type: String, attribute: "minutes-label" },
28
+ /**
29
+ * Label for the seconds unit (only shown when show-seconds is true).
30
+ * @type {string}
31
+ */
32
+ "seconds-label": { type: String, attribute: "seconds-label" },
33
+ /**
34
+ * Message displayed when the target date has passed.
35
+ * @type {string}
36
+ */
37
+ "past-message": { type: String, attribute: "past-message" },
38
+ /**
39
+ * Whether to show a seconds unit.
40
+ * @type {boolean}
41
+ */
42
+ "show-seconds": { type: Boolean, attribute: "show-seconds" },
43
+
44
+ /** @internal */
45
+ _days: { state: true },
46
+ /** @internal */
47
+ _hours: { state: true },
48
+ /** @internal */
49
+ _minutes: { state: true },
50
+ /** @internal */
51
+ _seconds: { state: true },
52
+ /** @internal */
53
+ _past: { state: true },
54
+ };
55
+
56
+ constructor() {
57
+ super();
58
+ this.target = "";
59
+ this["days-label"] = "days";
60
+ this["hours-label"] = "hours";
61
+ this["minutes-label"] = "minutes";
62
+ this["seconds-label"] = "seconds";
63
+ this["past-message"] = "The event has started!";
64
+ this["show-seconds"] = false;
65
+
66
+ this._days = "---";
67
+ this._hours = "--";
68
+ this._minutes = "--";
69
+ this._seconds = "--";
70
+ this._past = false;
71
+ this._intervalId = null;
72
+ }
73
+
74
+ connectedCallback() {
75
+ super.connectedCallback();
76
+ this._tick();
77
+ this._startInterval();
78
+ }
79
+
80
+ disconnectedCallback() {
81
+ super.disconnectedCallback();
82
+ this._stopInterval();
83
+ }
84
+
85
+ updated(changed) {
86
+ if (changed.has("target") || changed.has("show-seconds")) {
87
+ this._stopInterval();
88
+ this._tick();
89
+ this._startInterval();
90
+ }
91
+ this._animateChangedValues(changed);
92
+ }
93
+
94
+ _startInterval() {
95
+ const interval = this["show-seconds"] ? 1000 : 60_000;
96
+ this._intervalId = setInterval(() => this._tick(), interval);
97
+ }
98
+
99
+ _stopInterval() {
100
+ if (this._intervalId !== null) {
101
+ clearInterval(this._intervalId);
102
+ this._intervalId = null;
103
+ }
104
+ }
105
+
106
+ _tick() {
107
+ if (!this.target) return;
108
+
109
+ const targetDate = new Date(this.target);
110
+ const now = new Date();
111
+ const diff = targetDate - now;
112
+
113
+ if (diff <= 0) {
114
+ this._past = true;
115
+ this._stopInterval();
116
+ return;
117
+ }
118
+
119
+ this._past = false;
120
+ const totalSeconds = Math.floor(diff / 1000);
121
+ const totalMinutes = Math.floor(totalSeconds / 60);
122
+ const totalHours = Math.floor(totalMinutes / 60);
123
+
124
+ const pad = (n) => String(n).padStart(2, "0");
125
+
126
+ this._days = Math.floor(totalHours / 24);
127
+ this._hours = pad(totalHours % 24);
128
+ this._minutes = pad(totalMinutes % 60);
129
+ this._seconds = pad(totalSeconds % 60);
130
+ }
131
+
132
+ /** Re-trigger the flip animation on value elements that changed. */
133
+ _animateChangedValues(changed) {
134
+ const keys = ["_days", "_hours", "_minutes", "_seconds"];
135
+ for (const key of keys) {
136
+ if (!changed.has(key)) continue;
137
+ const el = this.shadowRoot?.querySelector(
138
+ `[data-unit="${key.slice(1)}"]`,
139
+ );
140
+ if (!el) continue;
141
+ el.classList.remove("countdown__value--flip");
142
+ // Force reflow so re-adding the class restarts the animation
143
+ void el.offsetWidth;
144
+ el.classList.add("countdown__value--flip");
145
+ }
146
+ }
147
+
148
+ render() {
149
+ if (!this.target) return nothing;
150
+
151
+ if (this._past) {
152
+ return html`<div class="countdown__past">${this["past-message"]}</div>`;
153
+ }
154
+
155
+ return html`
156
+ <div class="countdown" role="timer" aria-label="Countdown">
157
+ <div class="countdown__unit">
158
+ <span class="countdown__value" data-unit="days">${this._days}</span>
159
+ <span class="countdown__label">${this["days-label"]}</span>
160
+ </div>
161
+ <div class="countdown__unit">
162
+ <span class="countdown__value" data-unit="hours">${this._hours}</span>
163
+ <span class="countdown__label">${this["hours-label"]}</span>
164
+ </div>
165
+ <div class="countdown__unit">
166
+ <span class="countdown__value" data-unit="minutes">${this._minutes}</span>
167
+ <span class="countdown__label">${this["minutes-label"]}</span>
168
+ </div>
169
+ ${this["show-seconds"]
170
+ ? html`
171
+ <div class="countdown__unit">
172
+ <span class="countdown__value" data-unit="seconds">${this._seconds}</span>
173
+ <span class="countdown__label">${this["seconds-label"]}</span>
174
+ </div>
175
+ `
176
+ : nothing}
177
+ </div>
178
+ `;
179
+ }
180
+ }
@@ -0,0 +1,62 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :host {
8
+ display: block;
9
+ }
10
+
11
+ .countdown {
12
+ display: flex;
13
+ justify-content: center;
14
+ gap: var(--g-theme-spacing-lg, 2rem);
15
+ padding: var(--g-theme-spacing-lg, 2rem) var(--g-theme-spacing-md, 1rem);
16
+ text-align: center;
17
+ }
18
+
19
+ .countdown__unit {
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: center;
23
+ gap: var(--g-theme-spacing-xs, 0.25rem);
24
+ }
25
+
26
+ .countdown__value {
27
+ font-size: var(--g-theme-typography-h1-font-size, 6.25rem);
28
+ font-weight: var(--g-theme-typography-h1-font-weight, 900);
29
+ line-height: var(--g-theme-typography-h1-line-height, 1.1);
30
+ color: var(--g-theme-color-content-default);
31
+ font-variant-numeric: tabular-nums;
32
+ }
33
+
34
+ .countdown__value--flip {
35
+ animation: countdown-flip 0.35s ease-out;
36
+ }
37
+
38
+ .countdown__label {
39
+ font-size: var(--g-theme-typography-body-sm-font-size, 0.875rem);
40
+ color: var(--g-theme-color-content-secondary);
41
+ text-transform: uppercase;
42
+ letter-spacing: 0.05em;
43
+ }
44
+
45
+ .countdown__past {
46
+ font-size: var(--g-theme-typography-h2-font-size, 3rem);
47
+ font-weight: var(--g-theme-typography-h2-font-weight, 900);
48
+ color: var(--g-theme-color-content-default);
49
+ text-align: center;
50
+ padding: var(--g-theme-spacing-lg, 2rem) var(--g-theme-spacing-md, 1rem);
51
+ }
52
+
53
+ @keyframes countdown-flip {
54
+ 0% {
55
+ opacity: 0;
56
+ transform: translateY(0.25em);
57
+ }
58
+ 100% {
59
+ opacity: 1;
60
+ transform: translateY(0);
61
+ }
62
+ }
@@ -0,0 +1,6 @@
1
+ import { GrantCodesCountdown } from "./countdown.component.js";
2
+
3
+ export * from "./countdown.component.js";
4
+ export default GrantCodesCountdown;
5
+
6
+ customElements.define("grantcodes-countdown", GrantCodesCountdown);
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import { createComponent } from "@lit/react";
3
+ import { GrantCodesCountdown } from "./countdown.js";
4
+
5
+ export const Countdown = createComponent({
6
+ tagName: "grantcodes-countdown",
7
+ elementClass: GrantCodesCountdown,
8
+ react: React,
9
+ });
@@ -0,0 +1,65 @@
1
+ import { getStorybookHelpers } from "@wc-toolkit/storybook-helpers";
2
+ import "./countdown.js";
3
+
4
+ const { events, args, argTypes } =
5
+ getStorybookHelpers("grantcodes-countdown");
6
+
7
+ const meta = {
8
+ title: "Components/Countdown",
9
+ component: "grantcodes-countdown",
10
+ args,
11
+ argTypes,
12
+ parameters: {
13
+ actions: {
14
+ handles: events,
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ /**
22
+ * Default countdown to a future date.
23
+ */
24
+ export const Default = {
25
+ args: {
26
+ target: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
27
+ "days-label": "days",
28
+ "hours-label": "hours",
29
+ "minutes-label": "minutes",
30
+ },
31
+ };
32
+
33
+ /**
34
+ * Countdown with seconds visible.
35
+ */
36
+ export const WithSeconds = {
37
+ args: {
38
+ target: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
39
+ "show-seconds": true,
40
+ },
41
+ };
42
+
43
+ /**
44
+ * Countdown with custom Spanish labels.
45
+ */
46
+ export const SpanishLabels = {
47
+ args: {
48
+ target: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
49
+ "days-label": "días",
50
+ "hours-label": "horas",
51
+ "minutes-label": "minutos",
52
+ "seconds-label": "segundos",
53
+ "past-message": "¡El evento ha comenzado!",
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Countdown that has already passed — shows the past message.
59
+ */
60
+ export const PastEvent = {
61
+ args: {
62
+ target: "2020-01-01T00:00:00Z",
63
+ "past-message": "This event has already happened!",
64
+ },
65
+ };
@@ -0,0 +1 @@
1
+ export * from "./countdown.js";
@@ -1,3 +1,9 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
1
7
  :host {
2
8
  display: block;
3
9
  }
@@ -1,3 +1,8 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
1
6
 
2
7
  .dialog {
3
8
  width: 100%;
@@ -1,3 +1,9 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
1
7
  :host {
2
8
  display: block;
3
9
  }
@@ -12,6 +12,8 @@
12
12
 
13
13
  .footer {
14
14
  margin-block-start: auto;
15
+ border-block-start: 1px solid var(--g-theme-color-border-subtle);
16
+ background-color: var(--g-theme-color-background-subtle);
15
17
  }
16
18
 
17
19
  .footer__container {
@@ -65,7 +67,7 @@
65
67
 
66
68
  /* Slotted content styling */
67
69
  ::slotted(*) {
68
- font-size: var(--g-typography-font-size-14);
70
+ font: var(--g-typography-font-body-sm);
69
71
  color: var(--g-theme-color-content-secondary);
70
72
  }
71
73
 
@@ -1,3 +1,9 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
1
7
  .form-field {
2
8
  display: flex;
3
9
  flex-direction: column;
@@ -12,6 +12,7 @@ export class GrantCodesGalleryImage extends LitElement {
12
12
  alt: { type: String },
13
13
  caption: { type: String },
14
14
  thumbnail: { type: String },
15
+ filmstrip: { type: Boolean, reflect: true },
15
16
  };
16
17
 
17
18
  constructor() {
@@ -23,6 +24,7 @@ export class GrantCodesGalleryImage extends LitElement {
23
24
  this.alt = "";
24
25
  this.caption = "";
25
26
  this.thumbnail = "";
27
+ this.filmstrip = false;
26
28
  }
27
29
 
28
30
  captionTemplate() {
@@ -3,17 +3,46 @@ import { html } from "lit/static-html.js";
3
3
  import galleryStyles from "./gallery.css" with { type: "css" };
4
4
 
5
5
  export class GrantCodesGallery extends LitElement {
6
- // Styles are scoped to this element: they won't conflict with styles
7
- // on the main page or in other components. Styling API can be exposed
8
- // via CSS custom properties.
9
6
  static styles = [galleryStyles];
10
7
 
8
+ static properties = {
9
+ filmstrip: { type: Boolean, reflect: true },
10
+ };
11
+
11
12
  /** @type {any[]} */
12
13
  images = [];
13
14
 
14
- // Define reactive properties--updating a reactive property causes
15
- // the component to update.
16
- // @property() label = 'Button Label'
15
+ constructor() {
16
+ super();
17
+ this.filmstrip = false;
18
+ }
19
+
20
+ firstUpdated() {
21
+ const slot = this.renderRoot.querySelector(".gallery__slot");
22
+ if (slot) {
23
+ slot.addEventListener("slotchange", () => this._updateChildren());
24
+ this._updateChildren();
25
+ }
26
+ }
27
+
28
+ updated(changedProperties) {
29
+ if (changedProperties.has("filmstrip")) {
30
+ this._updateChildren();
31
+ }
32
+ }
33
+
34
+ _updateChildren() {
35
+ const slot = this.renderRoot.querySelector(".gallery__slot");
36
+ if (!slot) return;
37
+ const assigned = slot.assignedElements({ flatten: true });
38
+ for (const el of assigned) {
39
+ if (this.filmstrip) {
40
+ el.setAttribute("filmstrip", "");
41
+ } else {
42
+ el.removeAttribute("filmstrip");
43
+ }
44
+ }
45
+ }
17
46
 
18
47
  render() {
19
48
  return html`
@@ -1,3 +1,8 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
1
6
 
2
7
  .gallery {
3
8
  display: block;
@@ -50,3 +55,71 @@
50
55
  color: white;
51
56
  }
52
57
 
58
+ /* =============================================
59
+ FILMSTRIP VARIANT
60
+ ============================================= */
61
+
62
+ /* Gallery container in filmstrip mode */
63
+ :host([filmstrip]) .gallery {
64
+ overflow: hidden;
65
+ }
66
+
67
+ :host([filmstrip]) .gallery__slot {
68
+ display: flex;
69
+ flex-wrap: nowrap;
70
+ overflow-x: auto;
71
+ overflow-y: hidden;
72
+ block-size: var(--filmstrip-height, 240px);
73
+ gap: var(--gallery-gap, 4px);
74
+ scroll-snap-type: x mandatory;
75
+ overscroll-behavior-x: contain;
76
+ scrollbar-width: none;
77
+ }
78
+
79
+ :host([filmstrip]) .gallery__slot::-webkit-scrollbar {
80
+ display: none;
81
+ }
82
+
83
+ :host([filmstrip]) .gallery__slot::slotted(*) {
84
+ flex: 0 0 auto;
85
+ block-size: 100%;
86
+ scroll-snap-align: start;
87
+ }
88
+
89
+ /* Scroll-driven reveal animation */
90
+ @supports (animation-timeline: view()) {
91
+ @media (prefers-reduced-motion: no-preference) {
92
+ :host([filmstrip]) .gallery__slot::slotted(*) {
93
+ animation: filmstrip-reveal linear both;
94
+ animation-timeline: view(inline);
95
+ animation-range: entry 0% entry 60%;
96
+ }
97
+ }
98
+ }
99
+
100
+ @keyframes filmstrip-reveal {
101
+ from {
102
+ opacity: 0.3;
103
+ scale: 0.92;
104
+ }
105
+ to {
106
+ opacity: 1;
107
+ scale: 1;
108
+ }
109
+ }
110
+
111
+ /* Gallery-image host in filmstrip mode */
112
+ :host([filmstrip]) .gallery__image {
113
+ block-size: 100%;
114
+ width: fit-content;
115
+ }
116
+
117
+ :host([filmstrip]) .gallery__image img {
118
+ block-size: 100%;
119
+ width: auto;
120
+ aspect-ratio: unset;
121
+ max-inline-size: none;
122
+ object-fit: cover;
123
+ object-position: center;
124
+ }
125
+
@@ -58,3 +58,45 @@ const meta = {
58
58
  export default meta;
59
59
 
60
60
  export const Gallery = {};
61
+
62
+ const filmstripImages = [
63
+ { w: 400, h: 240 },
64
+ { w: 160, h: 240 },
65
+ { w: 360, h: 240 },
66
+ { w: 180, h: 240 },
67
+ { w: 420, h: 240 },
68
+ { w: 160, h: 240 },
69
+ { w: 380, h: 240 },
70
+ { w: 200, h: 240 },
71
+ { w: 440, h: 240 },
72
+ { w: 160, h: 240 },
73
+ { w: 350, h: 240 },
74
+ { w: 170, h: 240 },
75
+ ];
76
+
77
+ export const Filmstrip = {
78
+ args: {
79
+ filmstrip: true,
80
+ },
81
+ render: (args) =>
82
+ template(
83
+ args,
84
+ html`${filmstripImages.map(
85
+ ({ w, h }, i) =>
86
+ html`<grantcodes-gallery-image
87
+ src="https://picsum.photos/seed/${i + 10}/${w}/${h}"
88
+ alt="Photo ${i + 1}"
89
+ width="${w}"
90
+ height="${h}"
91
+ ></grantcodes-gallery-image>`,
92
+ )}`,
93
+ ),
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story:
98
+ "Filmstrip variant: images in a scrollable horizontal row at uniform height. Uses scroll-snap and scroll-driven animations.",
99
+ },
100
+ },
101
+ },
102
+ };