@grantcodes/ui 2.0.2 → 2.1.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.
Files changed (179) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/custom-elements.json +1926 -191
  3. package/package.json +22 -21
  4. package/src/components/accordion/accordion.component.js +33 -0
  5. package/src/components/accordion/accordion.js +6 -0
  6. package/src/components/accordion/accordion.stories.js +88 -0
  7. package/src/components/accordion/accordion.styles.js +66 -0
  8. package/src/components/accordion/index.js +6 -0
  9. package/src/components/app-bar/app-bar.component.js +1 -3
  10. package/src/components/app-bar/app-bar.js +0 -2
  11. package/src/components/app-bar/app-bar.styles.js +222 -221
  12. package/src/components/app-bar/app-bar.test.js +58 -17
  13. package/src/components/app-bar/index.js +0 -2
  14. package/src/components/avatar/avatar.js +0 -12
  15. package/src/components/avatar/avatar.stories.js +0 -12
  16. package/src/components/avatar/avatar.styles.js +19 -19
  17. package/src/components/avatar/avatar.test.js +4 -4
  18. package/src/components/avatar/index.js +1 -13
  19. package/src/components/badge/badge.js +0 -2
  20. package/src/components/badge/badge.styles.js +78 -81
  21. package/src/components/badge/badge.test.js +18 -5
  22. package/src/components/badge/index.js +0 -2
  23. package/src/components/breadcrumb/breadcrumb.component.js +9 -10
  24. package/src/components/breadcrumb/breadcrumb.js +6 -4
  25. package/src/components/breadcrumb/breadcrumb.styles.js +86 -90
  26. package/src/components/breadcrumb/breadcrumb.test.js +15 -5
  27. package/src/components/breadcrumb/index.js +0 -2
  28. package/src/components/button/button.component.js +2 -2
  29. package/src/components/button/button.styles.js +58 -86
  30. package/src/components/button/button.test.js +8 -4
  31. package/src/components/button/index.js +1 -1
  32. package/src/components/button-group/button-group.test.js +0 -2
  33. package/src/components/button-group/index.js +1 -1
  34. package/src/components/card/card.component.js +40 -9
  35. package/src/components/card/card.js +3 -1
  36. package/src/components/card/card.stories.js +18 -5
  37. package/src/components/card/card.styles.js +46 -20
  38. package/src/components/card/card.test.js +0 -2
  39. package/src/components/card/index.js +1 -1
  40. package/src/components/code-preview/code-preview.component.js +9 -9
  41. package/src/components/code-preview/code-preview.js +0 -1
  42. package/src/components/code-preview/code-preview.styles.js +3 -3
  43. package/src/components/code-preview/code-preview.test.js +29 -8
  44. package/src/components/code-preview/index.js +1 -1
  45. package/src/components/container/container.component.js +1 -0
  46. package/src/components/container/container.js +0 -1
  47. package/src/components/container/container.stories.js +12 -4
  48. package/src/components/container/container.styles.js +37 -35
  49. package/src/components/container/container.test.js +0 -2
  50. package/src/components/container/index.js +1 -1
  51. package/src/components/cta/cta.component.js +108 -0
  52. package/src/components/cta/cta.js +6 -0
  53. package/src/components/cta/cta.stories.js +56 -0
  54. package/src/components/cta/cta.styles.js +64 -0
  55. package/src/components/cta/index.js +1 -0
  56. package/src/components/dialog/dialog.js +0 -1
  57. package/src/components/dialog/dialog.styles.js +8 -8
  58. package/src/components/dialog/dialog.test.js +11 -5
  59. package/src/components/dialog/index.js +1 -1
  60. package/src/components/dropdown/dropdown.component.js +5 -3
  61. package/src/components/dropdown/dropdown.js +6 -4
  62. package/src/components/dropdown/dropdown.styles.js +5 -5
  63. package/src/components/dropdown/dropdown.test.js +20 -4
  64. package/src/components/dropdown/index.js +0 -2
  65. package/src/components/dropzone/dropzone.component.js +7 -6
  66. package/src/components/dropzone/dropzone.styles.js +4 -4
  67. package/src/components/dropzone/dropzone.test.js +6 -4
  68. package/src/components/dropzone/index.js +1 -1
  69. package/src/components/feature-list/feature-list.component.js +130 -0
  70. package/src/components/feature-list/feature-list.js +6 -0
  71. package/src/components/feature-list/feature-list.stories.js +117 -0
  72. package/src/components/feature-list/feature-list.styles.js +82 -0
  73. package/src/components/feature-list/index.js +1 -0
  74. package/src/components/footer/footer-column.styles.js +46 -47
  75. package/src/components/footer/footer.js +6 -2
  76. package/src/components/footer/footer.styles.js +6 -6
  77. package/src/components/footer/footer.test.js +9 -4
  78. package/src/components/footer/index.js +1 -1
  79. package/src/components/form-field/form-field.component.js +1 -3
  80. package/src/components/form-field/form-field.js +0 -1
  81. package/src/components/form-field/form-field.styles.js +35 -37
  82. package/src/components/form-field/form-field.test.js +9 -4
  83. package/src/components/form-field/index.js +1 -1
  84. package/src/components/gallery/gallery-image.js +0 -1
  85. package/src/components/gallery/gallery.js +0 -1
  86. package/src/components/gallery/gallery.styles.js +1 -1
  87. package/src/components/gallery/gallery.test.js +5 -3
  88. package/src/components/gallery/index.js +2 -2
  89. package/src/components/hero/hero.component.js +66 -0
  90. package/src/components/hero/hero.js +6 -0
  91. package/src/components/hero/hero.stories.js +53 -0
  92. package/src/components/hero/hero.styles.js +46 -0
  93. package/src/components/hero/index.js +1 -0
  94. package/src/components/icon/icon.js +3 -2
  95. package/src/components/icon/icon.stories.js +2 -1
  96. package/src/components/icon/icon.styles.js +23 -21
  97. package/src/components/icon/icon.test.js +2 -3
  98. package/src/components/icon/index.js +1 -1
  99. package/src/components/loading/index.js +1 -1
  100. package/src/components/loading/loading.js +3 -2
  101. package/src/components/loading/loading.styles.js +1 -1
  102. package/src/components/loading/loading.test.js +0 -2
  103. package/src/components/logo-cloud/index.js +1 -0
  104. package/src/components/logo-cloud/logo-cloud.component.js +81 -0
  105. package/src/components/logo-cloud/logo-cloud.js +6 -0
  106. package/src/components/logo-cloud/logo-cloud.stories.js +107 -0
  107. package/src/components/logo-cloud/logo-cloud.styles.js +68 -0
  108. package/src/components/media-text/index.js +1 -0
  109. package/src/components/media-text/media-text.component.js +100 -0
  110. package/src/components/media-text/media-text.js +6 -0
  111. package/src/components/media-text/media-text.stories.js +69 -0
  112. package/src/components/media-text/media-text.styles.js +66 -0
  113. package/src/components/newsletter/index.js +1 -0
  114. package/src/components/newsletter/newsletter.component.js +101 -0
  115. package/src/components/newsletter/newsletter.js +6 -0
  116. package/src/components/newsletter/newsletter.stories.js +59 -0
  117. package/src/components/newsletter/newsletter.styles.js +89 -0
  118. package/src/components/notice/index.js +1 -1
  119. package/src/components/notice/notice.js +0 -1
  120. package/src/components/notice/notice.styles.js +7 -7
  121. package/src/components/notice/notice.test.js +15 -5
  122. package/src/components/pagination/index.js +1 -1
  123. package/src/components/pagination/pagination.stories.js +1 -3
  124. package/src/components/pagination/pagination.styles.js +1 -1
  125. package/src/components/pagination/pagination.test.js +9 -4
  126. package/src/components/pricing/index.js +1 -0
  127. package/src/components/pricing/pricing.component.js +119 -0
  128. package/src/components/pricing/pricing.js +6 -0
  129. package/src/components/pricing/pricing.stories.js +123 -0
  130. package/src/components/pricing/pricing.styles.js +135 -0
  131. package/src/components/sidebar/index.js +0 -2
  132. package/src/components/sidebar/sidebar.component.js +12 -10
  133. package/src/components/sidebar/sidebar.js +3 -3
  134. package/src/components/sidebar/sidebar.stories.js +0 -2
  135. package/src/components/sidebar/sidebar.styles.js +181 -186
  136. package/src/components/sidebar/sidebar.test.js +48 -13
  137. package/src/components/stats/index.js +1 -0
  138. package/src/components/stats/stats.component.js +73 -0
  139. package/src/components/stats/stats.js +6 -0
  140. package/src/components/stats/stats.stories.js +64 -0
  141. package/src/components/stats/stats.styles.js +66 -0
  142. package/src/components/tabs/index.js +2 -2
  143. package/src/components/tabs/internal/tabs-button.component.js +1 -1
  144. package/src/components/tabs/internal/tabs-button.js +0 -1
  145. package/src/components/tabs/tab.js +0 -1
  146. package/src/components/tabs/tabs.js +3 -2
  147. package/src/components/tabs/tabs.styles.js +84 -74
  148. package/src/components/testimonials/index.js +1 -0
  149. package/src/components/testimonials/testimonials.component.js +97 -0
  150. package/src/components/testimonials/testimonials.js +6 -0
  151. package/src/components/testimonials/testimonials.stories.js +78 -0
  152. package/src/components/testimonials/testimonials.styles.js +82 -0
  153. package/src/components/toast/index.js +0 -2
  154. package/src/components/toast/toast.component.js +1 -3
  155. package/src/components/toast/toast.js +10 -5
  156. package/src/components/toast/toast.stories.js +9 -5
  157. package/src/components/toast/toast.styles.js +199 -201
  158. package/src/components/toast/toast.test.js +38 -10
  159. package/src/components/tooltip/index.js +1 -1
  160. package/src/components/tooltip/tooltip.js +3 -2
  161. package/src/components/tooltip/tooltip.styles.js +3 -3
  162. package/src/components/tooltip/tooltip.test.js +10 -4
  163. package/src/css/base.css +8 -5
  164. package/src/css/colors.stories.js +27 -28
  165. package/src/css/elements/forms/input.css +9 -41
  166. package/src/css/elements/media/image.css +1 -1
  167. package/src/css/themes/todomap.css +1 -0
  168. package/src/css/tokens.stories.js +26 -21
  169. package/src/css/typography.css +1 -3
  170. package/src/css/util/focus-ring.css +30 -0
  171. package/src/css/util/index.css +1 -2
  172. package/src/lib/styles/focus-ring.styles.js +34 -0
  173. package/src/main.js +10 -1
  174. package/src/pages/agency.stories.js +164 -0
  175. package/src/pages/blog-post.stories.js +381 -0
  176. package/src/pages/saas-landing.stories.js +307 -0
  177. package/src/test-utils/assert-helpers.js +10 -8
  178. package/src/css/util/functions.css +0 -16
  179. package/src/css/util/mixins.css +0 -63
@@ -51,8 +51,10 @@ describe("Gallery Component", () => {
51
51
  it("should initialize images array", async () => {
52
52
  element = await fixture("grantcodes-gallery");
53
53
  assert.ok(Array.isArray(element.images), "Images should be an array");
54
- assert.strictEqual(element.images.length, 0, "Images array should be empty initially");
54
+ assert.strictEqual(
55
+ element.images.length,
56
+ 0,
57
+ "Images array should be empty initially",
58
+ );
55
59
  });
56
60
  });
57
-
58
-
@@ -1,2 +1,2 @@
1
- export * from "./gallery";
2
- export * from "./gallery-image";
1
+ export * from "./gallery.js";
2
+ export * from "./gallery-image.js";
@@ -0,0 +1,66 @@
1
+ import { LitElement, html } from "lit";
2
+ import { heroStyles } from "./hero.styles.js";
3
+ import "../button/button.js";
4
+
5
+ export class GrantCodesHero extends LitElement {
6
+ static styles = [heroStyles];
7
+
8
+ static properties = {
9
+ /**
10
+ * Main heading text.
11
+ * @type {string}
12
+ */
13
+ title: { type: String },
14
+ /**
15
+ * Supporting paragraph text shown below the title.
16
+ * @type {string}
17
+ */
18
+ subtitle: { type: String },
19
+ /**
20
+ * Label for the CTA button. Only rendered when `href` is also set.
21
+ * @type {string}
22
+ */
23
+ button: { type: String },
24
+ /**
25
+ * URL the CTA button links to.
26
+ * @type {string}
27
+ */
28
+ href: { type: String },
29
+ /**
30
+ * Controls the vertical height of the hero section.
31
+ * @type {'sm' | 'md' | 'lg'}
32
+ */
33
+ size: { type: String, reflect: true },
34
+ };
35
+
36
+ constructor() {
37
+ super();
38
+ this.title = "";
39
+ this.subtitle = "";
40
+ this.button = "";
41
+ this.href = "";
42
+ this.size = "md";
43
+ }
44
+
45
+ render() {
46
+ return html`
47
+ <section class="hero">
48
+ <div class="hero__container">
49
+ <h1 class="hero__title">${this.title}</h1>
50
+ ${
51
+ this.subtitle
52
+ ? html`<p class="hero__text">${this.subtitle}</p>`
53
+ : null
54
+ }
55
+ ${
56
+ this.href && this.button
57
+ ? html`<grantcodes-button href=${this.href}
58
+ >${this.button}</grantcodes-button
59
+ >`
60
+ : null
61
+ }
62
+ </div>
63
+ </section>
64
+ `;
65
+ }
66
+ }
@@ -0,0 +1,6 @@
1
+ import { GrantCodesHero } from "./hero.component.js";
2
+
3
+ export * from "./hero.component.js";
4
+ export default GrantCodesHero;
5
+
6
+ customElements.define("grantcodes-hero", GrantCodesHero);
@@ -0,0 +1,53 @@
1
+ import { getStorybookHelpers } from "@wc-toolkit/storybook-helpers";
2
+ import "./hero.js";
3
+
4
+ const { events, args, argTypes } = getStorybookHelpers("grantcodes-hero");
5
+
6
+ const meta = {
7
+ title: "Blocks/Hero",
8
+ component: "grantcodes-hero",
9
+ args,
10
+ argTypes,
11
+ parameters: {
12
+ actions: {
13
+ handles: events,
14
+ },
15
+ layout: "fullscreen",
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ /**
22
+ * Default hero with title, subtitle, and CTA.
23
+ */
24
+ export const Default = {
25
+ args: {
26
+ title: "Build something great",
27
+ subtitle: "A personal web component library with a custom design system.",
28
+ button: "Get started",
29
+ href: "/docs",
30
+ size: "md",
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Small hero — useful for inner page headers.
36
+ */
37
+ export const Small = {
38
+ args: {
39
+ title: "About this project",
40
+ subtitle: "A brief overview of what we're building.",
41
+ size: "sm",
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Large hero with no CTA.
47
+ */
48
+ export const Large = {
49
+ args: {
50
+ title: "Welcome",
51
+ size: "lg",
52
+ },
53
+ };
@@ -0,0 +1,46 @@
1
+ import { css } from "lit";
2
+
3
+ export const heroStyles = css`
4
+ :host {
5
+ display: block;
6
+ }
7
+
8
+ .hero {
9
+ display: flex;
10
+ width: 100%;
11
+ justify-content: center;
12
+ align-items: center;
13
+ min-height: 50vh;
14
+ text-align: center;
15
+ padding: var(--g-theme-spacing-2xl) var(--g-theme-spacing-md);
16
+ background: var(--g-theme-color-background-brand-knockout, #7c3aed);
17
+ }
18
+
19
+ .hero__container {
20
+ width: 100%;
21
+ max-width: 65ch;
22
+ }
23
+
24
+ .hero__title {
25
+ margin: 0 0 var(--g-theme-spacing-md);
26
+ font-weight: var(--g-typography-font-weight-900);
27
+ font-size: clamp(2rem, 5vw, 4rem);
28
+ color: var(--g-theme-color-content-brand-knockout, #ffffff);
29
+ }
30
+
31
+ .hero__text {
32
+ font-size: var(--g-theme-typography-title-default-font-size);
33
+ color: var(--g-theme-color-content-brand-knockout, #ffffff);
34
+ margin: 0 0 var(--g-theme-spacing-lg);
35
+ }
36
+
37
+ :host([size="sm"]) .hero {
38
+ min-height: 30vh;
39
+ padding: var(--g-theme-spacing-xl) var(--g-theme-spacing-md);
40
+ }
41
+
42
+ :host([size="lg"]) .hero {
43
+ min-height: 70vh;
44
+ padding: var(--g-theme-spacing-3xl) var(--g-theme-spacing-md);
45
+ }
46
+ `;
@@ -0,0 +1 @@
1
+ export * from "./hero.js";
@@ -3,5 +3,6 @@ import { GrantCodesIcon } from "./icon.component.js";
3
3
  export * from "./icon.component.js";
4
4
  export default GrantCodesIcon;
5
5
 
6
- customElements.define("grantcodes-icon", GrantCodesIcon);
7
-
6
+ if (!customElements.get("grantcodes-icon")) {
7
+ customElements.define("grantcodes-icon", GrantCodesIcon);
8
+ }
@@ -11,7 +11,8 @@ const meta = {
11
11
  export default meta;
12
12
 
13
13
  export const Icon = {
14
- render: () => html`<grantcodes-icon>${unsafeHTML(ArrowRight)}</grantcodes-icon>`,
14
+ render: () =>
15
+ html`<grantcodes-icon>${unsafeHTML(ArrowRight)}</grantcodes-icon>`,
15
16
  };
16
17
 
17
18
  export const MultipleIcons = {
@@ -1,28 +1,30 @@
1
1
  import { css } from "lit";
2
2
 
3
3
  export const iconStyles = css`
4
+ :host {
5
+ display: block;
6
+ }
4
7
 
5
- .icon {
6
- display: inline-block;
7
- color: inherit;
8
- width: 1em;
9
- height: 1em;
10
- }
8
+ .icon {
9
+ display: block;
10
+ color: inherit;
11
+ width: 1em;
12
+ height: 1em;
13
+ }
11
14
 
12
- .icon svg {
13
- display: block;
14
- width: 100%;
15
- height: 100%;
16
- object-fit: contain;
17
- object-position: center;
18
- }
15
+ .icon ::slotted(svg) {
16
+ display: block;
17
+ width: 100%;
18
+ height: 100%;
19
+ object-fit: contain;
20
+ object-position: center;
21
+ }
19
22
 
20
- .icon svg:not([fill]) {
21
- fill: currentColor;
22
- }
23
+ .icon ::slotted(svg:not([fill])) {
24
+ fill: currentColor;
25
+ }
23
26
 
24
- .icon svg:not([stoke]) {
25
- stroke: currentColor;
26
- }
27
-
28
- `;
27
+ .icon ::slotted(svg:not([stoke])) {
28
+ stroke: currentColor;
29
+ }
30
+ `;
@@ -44,7 +44,8 @@ describe("Icon Component", () => {
44
44
 
45
45
  it("should accept any SVG as slot content", async () => {
46
46
  element = await fixture("grantcodes-icon");
47
- element.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>';
47
+ element.innerHTML =
48
+ '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>';
48
49
 
49
50
  await element.updateComplete;
50
51
 
@@ -53,5 +54,3 @@ describe("Icon Component", () => {
53
54
  assert.ok(svg.querySelector("circle"), "SVG content should be preserved");
54
55
  });
55
56
  });
56
-
57
-
@@ -1 +1 @@
1
- export * from "./icon";
1
+ export * from "./icon.js";
@@ -1 +1 @@
1
- export * from "./loading";
1
+ export * from "./loading.js";
@@ -3,5 +3,6 @@ import { GrantCodesLoading } from "./loading.component.js";
3
3
  export * from "./loading.component.js";
4
4
  export default GrantCodesLoading;
5
5
 
6
- customElements.define("grantcodes-loading", GrantCodesLoading);
7
-
6
+ if (!customElements.get("grantcodes-loading")) {
7
+ customElements.define("grantcodes-loading", GrantCodesLoading);
8
+ }
@@ -40,4 +40,4 @@ export const loadingStyles = css`
40
40
  }
41
41
  }
42
42
 
43
- `;
43
+ `;
@@ -53,5 +53,3 @@ describe("Loading Component", () => {
53
53
  assert.ok(customSpinner, "Custom spinner should be slotted");
54
54
  });
55
55
  });
56
-
57
-
@@ -0,0 +1 @@
1
+ export * from "./logo-cloud.js";
@@ -0,0 +1,81 @@
1
+ import { LitElement, html } from "lit";
2
+ import { logoCloudStyles } from "./logo-cloud.styles.js";
3
+
4
+ export class GrantCodesLogoCloud extends LitElement {
5
+ static styles = [logoCloudStyles];
6
+
7
+ static properties = {
8
+ /**
9
+ * Optional label shown above the logos.
10
+ * @type {string}
11
+ */
12
+ title: { type: String },
13
+ /**
14
+ * Logo items as a JSON string array: `[{"name":"...","src":"...","alt":"...","href":"..."}]`.
15
+ * @type {string}
16
+ */
17
+ logos: { type: String },
18
+ };
19
+
20
+ constructor() {
21
+ super();
22
+ this.title = "";
23
+ this.logos = "[]";
24
+ }
25
+
26
+ get _logos() {
27
+ try {
28
+ return JSON.parse(this.logos);
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ render() {
35
+ const logos = this._logos;
36
+ return html`
37
+ <section class="logo-cloud">
38
+ <div class="logo-cloud__container">
39
+ ${
40
+ this.title
41
+ ? html`<p class="logo-cloud__title">${this.title}</p>`
42
+ : null
43
+ }
44
+ <ul class="logo-cloud__grid" role="list">
45
+ ${logos.map(
46
+ (logo) => html`
47
+ <li class="logo-cloud__item">
48
+ ${
49
+ logo.href
50
+ ? html`
51
+ <a
52
+ href=${logo.href}
53
+ class="logo-cloud__link"
54
+ aria-label=${logo.name}
55
+ >
56
+ <img
57
+ src=${logo.src}
58
+ alt=${logo.alt ?? logo.name}
59
+ class="logo-cloud__logo"
60
+ loading="lazy"
61
+ />
62
+ </a>
63
+ `
64
+ : html`
65
+ <img
66
+ src=${logo.src}
67
+ alt=${logo.alt ?? logo.name}
68
+ class="logo-cloud__logo"
69
+ loading="lazy"
70
+ />
71
+ `
72
+ }
73
+ </li>
74
+ `,
75
+ )}
76
+ </ul>
77
+ </div>
78
+ </section>
79
+ `;
80
+ }
81
+ }
@@ -0,0 +1,6 @@
1
+ import { GrantCodesLogoCloud } from "./logo-cloud.component.js";
2
+
3
+ export * from "./logo-cloud.component.js";
4
+ export default GrantCodesLogoCloud;
5
+
6
+ customElements.define("grantcodes-logo-cloud", GrantCodesLogoCloud);
@@ -0,0 +1,107 @@
1
+ import { getStorybookHelpers } from "@wc-toolkit/storybook-helpers";
2
+ import "./logo-cloud.js";
3
+
4
+ const { events, args, argTypes } = getStorybookHelpers("grantcodes-logo-cloud");
5
+
6
+ const meta = {
7
+ title: "Blocks/LogoCloud",
8
+ component: "grantcodes-logo-cloud",
9
+ args,
10
+ argTypes,
11
+ parameters: {
12
+ actions: {
13
+ handles: events,
14
+ },
15
+ layout: "fullscreen",
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ const sampleLogos = JSON.stringify([
22
+ {
23
+ name: "Acme Corp",
24
+ src: "https://placehold.co/120x40?text=Acme",
25
+ alt: "Acme Corp logo",
26
+ },
27
+ {
28
+ name: "Globex",
29
+ src: "https://placehold.co/120x40?text=Globex",
30
+ alt: "Globex logo",
31
+ },
32
+ {
33
+ name: "Initech",
34
+ src: "https://placehold.co/120x40?text=Initech",
35
+ alt: "Initech logo",
36
+ },
37
+ {
38
+ name: "Umbrella",
39
+ src: "https://placehold.co/120x40?text=Umbrella",
40
+ alt: "Umbrella Corp logo",
41
+ },
42
+ {
43
+ name: "Hooli",
44
+ src: "https://placehold.co/120x40?text=Hooli",
45
+ alt: "Hooli logo",
46
+ },
47
+ ]);
48
+
49
+ /**
50
+ * Logo cloud with a label and linked logos.
51
+ */
52
+ export const Default = {
53
+ args: {
54
+ title: "Trusted by teams at",
55
+ logos: JSON.stringify([
56
+ {
57
+ name: "Acme Corp",
58
+ src: "https://placehold.co/120x40?text=Acme",
59
+ alt: "Acme Corp logo",
60
+ href: "https://example.com",
61
+ },
62
+ {
63
+ name: "Globex",
64
+ src: "https://placehold.co/120x40?text=Globex",
65
+ alt: "Globex logo",
66
+ href: "https://example.com",
67
+ },
68
+ {
69
+ name: "Initech",
70
+ src: "https://placehold.co/120x40?text=Initech",
71
+ alt: "Initech logo",
72
+ href: "https://example.com",
73
+ },
74
+ {
75
+ name: "Umbrella",
76
+ src: "https://placehold.co/120x40?text=Umbrella",
77
+ alt: "Umbrella Corp logo",
78
+ href: "https://example.com",
79
+ },
80
+ {
81
+ name: "Hooli",
82
+ src: "https://placehold.co/120x40?text=Hooli",
83
+ alt: "Hooli logo",
84
+ href: "https://example.com",
85
+ },
86
+ ]),
87
+ },
88
+ };
89
+
90
+ /**
91
+ * Logo cloud without a title heading.
92
+ */
93
+ export const NoTitle = {
94
+ args: {
95
+ logos: sampleLogos,
96
+ },
97
+ };
98
+
99
+ /**
100
+ * Logo cloud with unlinked logos — read-only brand display.
101
+ */
102
+ export const Unlinked = {
103
+ args: {
104
+ title: "Built with",
105
+ logos: sampleLogos,
106
+ },
107
+ };
@@ -0,0 +1,68 @@
1
+ import { css } from "lit";
2
+
3
+ export const logoCloudStyles = css`
4
+ :host {
5
+ display: block;
6
+ }
7
+
8
+ .logo-cloud {
9
+ padding-block: var(--g-theme-spacing-2xl);
10
+ padding-inline: var(--g-theme-spacing-md);
11
+ border-top: 1px solid var(--g-theme-color-border-default);
12
+ border-bottom: 1px solid var(--g-theme-color-border-default);
13
+ }
14
+
15
+ .logo-cloud__container {
16
+ max-width: 1200px;
17
+ margin: 0 auto;
18
+ }
19
+
20
+ .logo-cloud__title {
21
+ margin: 0 0 var(--g-theme-spacing-xl);
22
+ font-size: var(--g-theme-typography-meta-default-font-size);
23
+ font-weight: var(--g-theme-typography-meta-default-font-weight);
24
+ letter-spacing: var(--g-theme-typography-meta-default-letter-spacing);
25
+ text-transform: uppercase;
26
+ text-align: center;
27
+ color: var(--g-theme-color-content-secondary);
28
+ }
29
+
30
+ .logo-cloud__grid {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ justify-content: center;
35
+ gap: var(--g-theme-spacing-lg) var(--g-theme-spacing-2xl);
36
+ padding: 0;
37
+ margin: 0;
38
+ list-style: none;
39
+ }
40
+
41
+ .logo-cloud__link {
42
+ display: block;
43
+ opacity: 0.7;
44
+ transition: opacity 0.2s;
45
+ }
46
+
47
+ .logo-cloud__link:hover {
48
+ opacity: 1;
49
+ }
50
+
51
+ .logo-cloud__logo {
52
+ display: block;
53
+ block-size: var(--g-theme-spacing-xl);
54
+ inline-size: auto;
55
+ max-inline-size: 140px;
56
+ object-fit: contain;
57
+ filter: grayscale(1);
58
+ transition: filter 0.2s;
59
+ }
60
+
61
+ .logo-cloud__link:hover .logo-cloud__logo {
62
+ filter: none;
63
+ }
64
+
65
+ .logo-cloud__item:not(:has(.logo-cloud__link)) .logo-cloud__logo {
66
+ filter: none;
67
+ }
68
+ `;
@@ -0,0 +1 @@
1
+ export * from "./media-text.js";
@@ -0,0 +1,100 @@
1
+ import { LitElement, html } from "lit";
2
+ import { mediaTextStyles } from "./media-text.styles.js";
3
+ import "../button/button.js";
4
+
5
+ export class GrantCodesMediaText extends LitElement {
6
+ static styles = [mediaTextStyles];
7
+
8
+ static properties = {
9
+ /**
10
+ * Section heading.
11
+ * @type {string}
12
+ */
13
+ title: { type: String },
14
+ /**
15
+ * Body text paragraph.
16
+ * @type {string}
17
+ */
18
+ text: { type: String },
19
+ /**
20
+ * Media object as a JSON string: `{"src":"...","alt":"...","kind":"image|video"}`.
21
+ * @type {string}
22
+ */
23
+ media: { type: String },
24
+ /**
25
+ * When true, the media appears on the right and text on the left.
26
+ * @type {boolean}
27
+ */
28
+ reverse: { type: Boolean, reflect: true },
29
+ /**
30
+ * Optional CTA as a JSON string: `{"label":"...","href":"..."}`.
31
+ * @type {string}
32
+ */
33
+ cta: { type: String },
34
+ };
35
+
36
+ constructor() {
37
+ super();
38
+ this.title = "";
39
+ this.text = "";
40
+ this.media = "";
41
+ this.reverse = false;
42
+ this.cta = "";
43
+ }
44
+
45
+ get _media() {
46
+ try {
47
+ return this.media ? JSON.parse(this.media) : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ get _cta() {
54
+ try {
55
+ return this.cta ? JSON.parse(this.cta) : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ render() {
62
+ const media = this._media;
63
+ const cta = this._cta;
64
+ return html`
65
+ <section class="media-text">
66
+ <div class="media-text__container">
67
+ <div class="media-text__media">
68
+ ${
69
+ media?.kind === "video"
70
+ ? html`<video
71
+ src=${media.src}
72
+ class="media-text__video"
73
+ controls
74
+ preload="metadata"
75
+ aria-label=${media.alt ?? ""}
76
+ ></video>`
77
+ : html`<img
78
+ src=${media?.src ?? ""}
79
+ alt=${media?.alt ?? ""}
80
+ class="media-text__image"
81
+ loading="lazy"
82
+ />`
83
+ }
84
+ </div>
85
+ <div class="media-text__content">
86
+ <h2 class="media-text__title">${this.title}</h2>
87
+ <p class="media-text__text">${this.text}</p>
88
+ ${
89
+ cta
90
+ ? html`<grantcodes-button href=${cta.href}
91
+ >${cta.label}</grantcodes-button
92
+ >`
93
+ : null
94
+ }
95
+ </div>
96
+ </div>
97
+ </section>
98
+ `;
99
+ }
100
+ }