@grantcodes/ui 2.5.1 → 2.7.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 (56) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/custom-elements.json +617 -19
  3. package/package.json +4 -4
  4. package/src/components/accordion/accordion.component.js +4 -1
  5. package/src/components/accordion/accordion.css +26 -18
  6. package/src/components/app-bar/app-bar.component.js +45 -9
  7. package/src/components/app-bar/app-bar.css +119 -88
  8. package/src/components/app-bar/app-bar.stories.js +75 -37
  9. package/src/components/app-bar/app-bar.test.js +7 -1
  10. package/src/components/app-bar/nav-link.component.js +15 -0
  11. package/src/components/app-bar/nav-link.css +58 -0
  12. package/src/components/app-bar/nav-link.js +6 -0
  13. package/src/components/app-bar/nav-link.react.js +9 -0
  14. package/src/components/code-preview/code-preview.css +5 -0
  15. package/src/components/container/container.css +6 -0
  16. package/src/components/countdown/countdown.component.js +180 -0
  17. package/src/components/countdown/countdown.css +62 -0
  18. package/src/components/countdown/countdown.js +6 -0
  19. package/src/components/countdown/countdown.react.js +9 -0
  20. package/src/components/countdown/countdown.stories.js +65 -0
  21. package/src/components/countdown/index.js +1 -0
  22. package/src/components/cta/cta.css +6 -0
  23. package/src/components/dialog/dialog.css +5 -0
  24. package/src/components/feature-list/feature-list.css +6 -0
  25. package/src/components/footer/footer.css +3 -1
  26. package/src/components/form-field/form-field.css +6 -0
  27. package/src/components/gallery/gallery.css +5 -0
  28. package/src/components/hero/hero.component.js +7 -0
  29. package/src/components/hero/hero.css +18 -1
  30. package/src/components/hero/hero.stories.js +30 -0
  31. package/src/components/icon/icon.css +6 -0
  32. package/src/components/loading/loading.css +5 -0
  33. package/src/components/logo-cloud/logo-cloud.css +6 -0
  34. package/src/components/map/index.js +1 -0
  35. package/src/components/map/map.component.js +135 -0
  36. package/src/components/map/map.css +41 -0
  37. package/src/components/map/map.js +6 -0
  38. package/src/components/map/map.react.js +9 -0
  39. package/src/components/map/map.stories.js +68 -0
  40. package/src/components/media-text/media-text.css +6 -0
  41. package/src/components/newsletter/newsletter.css +6 -0
  42. package/src/components/notice/notice.css +5 -0
  43. package/src/components/pagination/pagination.css +5 -0
  44. package/src/components/pricing/pricing.css +6 -0
  45. package/src/components/stats/stats.css +6 -0
  46. package/src/components/testimonials/testimonials.css +6 -0
  47. package/src/components/tooltip/tooltip.css +5 -0
  48. package/src/css/all.css +3 -1
  49. package/src/css/base.css +6 -247
  50. package/src/css/elements/a.css +8 -8
  51. package/src/css/reset.css +246 -0
  52. package/src/lib/styles/all.css +2 -0
  53. package/src/main.js +2 -0
  54. package/src/pages/blog-post.stories.js +7 -19
  55. package/src/react.js +3 -0
  56. package/src/types.d.ts +18 -0
@@ -0,0 +1,58 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :host {
8
+ display: inline-flex;
9
+ }
10
+
11
+ .nav-link {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ border-radius: var(--g-theme-border-radius-md);
15
+ background-color: transparent;
16
+ transition: background-color 0.15s ease;
17
+ }
18
+
19
+ .nav-link:hover {
20
+ background-color: color-mix(in srgb, var(--g-theme-color-content-default) 8%, transparent);
21
+ }
22
+
23
+ /*
24
+ * Override light-DOM :where(a) base styles for slotted links.
25
+ * ::slotted() has lower cascade priority than outer-tree normal rules,
26
+ * so !important is needed for properties that the base layer also sets
27
+ * (color, text-decoration, opacity). This is a legitimate encapsulation
28
+ * use-case — nav links must look like ghost buttons regardless of what
29
+ * global link styles exist in the light DOM.
30
+ */
31
+ ::slotted(a),
32
+ ::slotted(a:visited),
33
+ ::slotted(a:hover),
34
+ ::slotted(a:active),
35
+ ::slotted(a:focus-visible) {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ color: var(--g-theme-color-content-default) !important;
39
+ text-decoration: none !important;
40
+ opacity: 1 !important;
41
+ padding-block: var(--g-theme-spacing-xs, 0.25rem);
42
+ padding-inline: var(--g-theme-spacing-sm);
43
+ font-family: var(--g-theme-typography-label-font-family);
44
+ font-weight: var(--g-theme-typography-label-font-weight);
45
+ white-space: nowrap;
46
+ }
47
+
48
+ ::slotted(a:focus-visible) {
49
+ outline: var(--g-theme-focus-ring-width-default) solid var(--g-theme-focus-ring-color-default);
50
+ outline-offset: var(--g-theme-focus-ring-offset-default);
51
+ border-radius: var(--g-theme-border-radius-md);
52
+ }
53
+
54
+ @media (prefers-reduced-motion: reduce) {
55
+ .nav-link {
56
+ transition: none;
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import { GrantCodesNavLink } from "./nav-link.component.js";
2
+
3
+ export * from "./nav-link.component.js";
4
+ export default GrantCodesNavLink;
5
+
6
+ customElements.define("grantcodes-nav-link", GrantCodesNavLink);
@@ -0,0 +1,9 @@
1
+ import React from "react"
2
+ import { createComponent } from "@lit/react"
3
+ import { GrantCodesNavLink } from "./nav-link.js"
4
+
5
+ export const NavLink = createComponent({
6
+ tagName: "grantcodes-nav-link",
7
+ elementClass: GrantCodesNavLink,
8
+ react: React,
9
+ })
@@ -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;
@@ -1,3 +1,8 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
1
6
 
2
7
  .gallery {
3
8
  display: block;
@@ -31,6 +31,11 @@ export class GrantCodesHero extends LitElement {
31
31
  * @type {'sm' | 'md' | 'lg'}
32
32
  */
33
33
  size: { type: String, reflect: true },
34
+ /**
35
+ * Whether to center-align text content.
36
+ * @type {boolean}
37
+ */
38
+ center: { type: Boolean, reflect: true },
34
39
  };
35
40
 
36
41
  constructor() {
@@ -40,6 +45,7 @@ export class GrantCodesHero extends LitElement {
40
45
  this.button = "";
41
46
  this.href = "";
42
47
  this.size = "md";
48
+ this.center = false;
43
49
  }
44
50
 
45
51
  render() {
@@ -59,6 +65,7 @@ export class GrantCodesHero extends LitElement {
59
65
  >`
60
66
  : null
61
67
  }
68
+ <slot></slot>
62
69
  </div>
63
70
  </section>
64
71
  `;
@@ -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
  }
@@ -8,7 +14,6 @@
8
14
  justify-content: center;
9
15
  align-items: center;
10
16
  min-height: 50vh;
11
- text-align: center;
12
17
  padding: var(--g-theme-spacing-2xl) var(--g-theme-spacing-md);
13
18
  background: var(--g-theme-color-background-brand-knockout, #7c3aed);
14
19
  }
@@ -19,6 +24,14 @@
19
24
  gap: var(--g-theme-spacing-lg);
20
25
  }
21
26
 
27
+ :host([center]) .hero {
28
+ text-align: center;
29
+ }
30
+
31
+ :host([center]) .hero__container {
32
+ margin-inline: auto;
33
+ }
34
+
22
35
  .hero__title {
23
36
  font: var(--g-theme-typography-h1);
24
37
  font-size: clamp(2rem, 5vw, 4rem);
@@ -39,3 +52,7 @@
39
52
  min-height: 70vh;
40
53
  padding: var(--g-theme-spacing-3xl) var(--g-theme-spacing-md);
41
54
  }
55
+
56
+ ::slotted(*) {
57
+ color: var(--g-theme-color-content-brand-knockout, #ffffff);
58
+ }
@@ -1,4 +1,5 @@
1
1
  import { getStorybookHelpers } from "@wc-toolkit/storybook-helpers";
2
+ import { html } from "lit";
2
3
  import "./hero.js";
3
4
 
4
5
  const { events, args, argTypes } = getStorybookHelpers("grantcodes-hero");
@@ -31,6 +32,35 @@ export const Default = {
31
32
  },
32
33
  };
33
34
 
35
+ /**
36
+ * Centered hero — explicitly center-aligns text content.
37
+ */
38
+ export const Centered = {
39
+ args: {
40
+ title: "Build something great",
41
+ subtitle: "A personal web component library with a custom design system.",
42
+ button: "Get started",
43
+ href: "/docs",
44
+ size: "md",
45
+ center: true,
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Hero with slotted content — custom HTML inside the hero.
51
+ */
52
+ export const WithSlottedContent = {
53
+ render: () => html`
54
+ <grantcodes-hero
55
+ title="Event countdown"
56
+ subtitle="Something exciting is coming soon."
57
+ center
58
+ >
59
+ <p style="font-size: 1.5rem; margin-top: 1rem;">Custom slotted content here</p>
60
+ </grantcodes-hero>
61
+ `,
62
+ };
63
+
34
64
  /**
35
65
  * Small hero — useful for inner page headers.
36
66
  */
@@ -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
  :host {
3
8
  --height: 0.5rem;
@@ -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
  }
@@ -0,0 +1 @@
1
+ export * from "./map.js";