@grantcodes/ui 2.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.0](https://github.com/grantcodes/ui/compare/ui-v2.7.0...ui-v2.8.0) (2026-04-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * **09-gallery-film-strip:** add filmstrip and marquee gallery variants ([0a4ff3e](https://github.com/grantcodes/ui/commit/0a4ff3e620bc21a6d291943acc72608d1c08d371))
9
+ * **ui:** gallery filmstrip variant ([d22a93d](https://github.com/grantcodes/ui/commit/d22a93d615874ca76fcf7525ea8d1dfa71721e2b))
10
+
3
11
  ## [2.7.0](https://github.com/grantcodes/ui/compare/ui-v2.6.0...ui-v2.7.0) (2026-04-06)
4
12
 
5
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grantcodes/ui",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "A personal component system built with Lit web components",
5
5
  "type": "module",
6
6
  "main": "src/main.js",
@@ -47,7 +47,7 @@
47
47
  "@lit/react": "^1.0.8",
48
48
  "lit": "^3.3.1",
49
49
  "shiki": "^3.17.1",
50
- "@grantcodes/style-dictionary": "^1.4.1"
50
+ "@grantcodes/style-dictionary": "^1.4.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@arcmantle/vite-plugin-import-css-sheet": "^1.0.12",
@@ -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`
@@ -55,3 +55,71 @@
55
55
  color: white;
56
56
  }
57
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
+ };
@@ -2,6 +2,7 @@ import { describe, it, afterEach } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
3
  import { fixture, cleanup } from "../../test-utils/index.js";
4
4
  import "./gallery.js";
5
+ import "./gallery-image.js";
5
6
 
6
7
  describe("Gallery Component", () => {
7
8
  let element;
@@ -57,4 +58,27 @@ describe("Gallery Component", () => {
57
58
  "Images array should be empty initially",
58
59
  );
59
60
  });
61
+
62
+ describe("Gallery filmstrip variant", () => {
63
+ let element;
64
+
65
+ afterEach(() => {
66
+ cleanup(element);
67
+ });
68
+
69
+ it("should have filmstrip property default to false", async () => {
70
+ element = await fixture("grantcodes-gallery");
71
+ assert.strictEqual(element.filmstrip, false, "filmstrip should default to false");
72
+ });
73
+
74
+ it("should reflect filmstrip attribute when property is set", async () => {
75
+ element = await fixture("grantcodes-gallery");
76
+ element.filmstrip = true;
77
+ await element.updateComplete;
78
+ assert.ok(
79
+ element.hasAttribute("filmstrip"),
80
+ "filmstrip attribute should be reflected",
81
+ );
82
+ });
83
+ });
60
84
  });