@ds-gn/ui 0.9.0 → 0.10.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.
@@ -1219,6 +1219,22 @@
1219
1219
  "description": "Space below the content.",
1220
1220
  "attribute": "padding-bottom",
1221
1221
  "reflects": true
1222
+ },
1223
+ {
1224
+ "kind": "field",
1225
+ "name": "motion",
1226
+ "type": {
1227
+ "text": "MotionType"
1228
+ },
1229
+ "description": "Animate the section in when it scrolls into view. Each heading block\ninside fades in its eyebrow, writes its heading letter by letter, then\nfades in its description and actions; each form block then fades in its\ninput and button together, then its note. A section already in view when\nit loads, and reduced motion, stay static.",
1230
+ "attribute": "motion",
1231
+ "reflects": true
1232
+ },
1233
+ {
1234
+ "kind": "field",
1235
+ "name": "_entrance",
1236
+ "privacy": "private",
1237
+ "default": "new SectionEntranceController( this, () => this.motion === motionValues.ON, )"
1222
1238
  }
1223
1239
  ],
1224
1240
  "superclass": {
@@ -1268,6 +1284,14 @@
1268
1284
  "text": "SectionSpaceType | undefined"
1269
1285
  },
1270
1286
  "description": "Space below the content."
1287
+ },
1288
+ {
1289
+ "name": "motion",
1290
+ "fieldName": "motion",
1291
+ "type": {
1292
+ "text": "MotionType"
1293
+ },
1294
+ "description": "Animate the section in when it scrolls into view. Each heading block\ninside fades in its eyebrow, writes its heading letter by letter, then\nfades in its description and actions; each form block then fades in its\ninput and button together, then its note. A section already in view when\nit loads, and reduced motion, stay static."
1271
1295
  }
1272
1296
  ]
1273
1297
  }
@@ -0,0 +1,37 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ type EntrancePart = {
3
+ kind: 'fade';
4
+ els: HTMLElement[];
5
+ } | {
6
+ kind: 'letters';
7
+ el: HTMLElement;
8
+ };
9
+ /**
10
+ * The entrance parts inside `root`, in document order. Each heading block
11
+ * contributes its eyebrow (fade), heading (letter by letter), description,
12
+ * then actions; each form block its input and button together, then its note.
13
+ */
14
+ export declare function sectionEntranceParts(root: Element): EntrancePart[];
15
+ /**
16
+ * Plays a section's entrance when it scrolls into view: the heading blocks
17
+ * and form blocks it contains animate in one timeline (see
18
+ * `sectionEntranceParts`). Decided from the first visibility report: a
19
+ * section already on screen, and reduced motion, stay static; one off screen
20
+ * holds its parts hidden (from script, so nothing is hidden when scripts do
21
+ * not run) and plays once when it comes into view.
22
+ */
23
+ export declare class SectionEntranceController implements ReactiveController {
24
+ private readonly host;
25
+ private readonly enabled;
26
+ private readonly motion;
27
+ private observer?;
28
+ private state?;
29
+ private parts;
30
+ constructor(host: ReactiveControllerHost & HTMLElement, enabled: () => boolean);
31
+ hostUpdated(): void;
32
+ hostDisconnected(): void;
33
+ private report;
34
+ private play;
35
+ private stopObserving;
36
+ }
37
+ export {};
@@ -1,6 +1,7 @@
1
1
  import '../../../types/global.d.ts';
2
2
  import { type CSSResultGroup } from 'lit';
3
3
  import { Base } from '../../../common/Base/Base.js';
4
+ import { type MotionType } from '../../../common/props/motion/motion.props.js';
4
5
  import { type ContainerPaddingType, type ContainerSizeType, type SectionSpaceType } from './Container.props.js';
5
6
  /** @slot - Constrained page content. */
6
7
  export declare class Container extends Base {
@@ -22,6 +23,15 @@ export declare class Container extends Base {
22
23
  paddingTop?: SectionSpaceType;
23
24
  /** Space below the content. */
24
25
  paddingBottom?: SectionSpaceType;
26
+ /**
27
+ * Animate the section in when it scrolls into view. Each heading block
28
+ * inside fades in its eyebrow, writes its heading letter by letter, then
29
+ * fades in its description and actions; each form block then fades in its
30
+ * input and button together, then its note. A section already in view when
31
+ * it loads, and reduced motion, stay static.
32
+ */
33
+ motion: MotionType;
34
+ private _entrance;
25
35
  render(): import("lit").TemplateResult<1>;
26
36
  }
27
37
  declare global {
@@ -91,6 +91,146 @@ class Base extends LitElement {
91
91
  }
92
92
  defineElement("dsgn-base", Base);
93
93
 
94
+ // common/controllers/SectionEntranceController.ts
95
+ import {
96
+ prefersReducedMotion,
97
+ splitText
98
+ } from "@ds-gn/motion";
99
+ import { MotionController } from "@ds-gn/motion/lit";
100
+ var HEADING_STAGGER = 20;
101
+ var HEADING_MAX_SPREAD = 500;
102
+ var HEADING_RISE = 4;
103
+ var MOTION_OVERLAP = 160;
104
+ var holdHidden = (el) => el.style.setProperty("opacity", "0");
105
+ var releaseHidden = (el) => el.style.removeProperty("opacity");
106
+ var ENTRANCE_BLOCKS = new Set(["dsgn-heading-block", "dsgn-form-block"]);
107
+ var hasContent = (el) => !["div", "span"].includes(el.localName) || el.children.length > 0 || (el.textContent ?? "").trim() !== "";
108
+ var childrenIn = (parent, ...slots) => Array.from(parent.children).filter((child) => child instanceof HTMLElement && slots.includes(child.slot) && hasContent(child));
109
+ function sectionEntranceParts(root) {
110
+ const parts = [];
111
+ const fade = (els) => {
112
+ if (els.length > 0)
113
+ parts.push({ kind: "fade", els });
114
+ };
115
+ Array.from(root.querySelectorAll("*")).filter((el) => ENTRANCE_BLOCKS.has(el.localName)).forEach((block) => {
116
+ if (block.localName === "dsgn-form-block") {
117
+ fade(childrenIn(block, "input", "button"));
118
+ fade(childrenIn(block, "note"));
119
+ return;
120
+ }
121
+ fade(childrenIn(block, "eyebrow"));
122
+ const [heading] = childrenIn(block, "heading");
123
+ if (heading)
124
+ parts.push({ kind: "letters", el: heading });
125
+ fade(childrenIn(block, "description"));
126
+ fade(childrenIn(block, "action-primary", "action-secondary"));
127
+ });
128
+ return parts;
129
+ }
130
+ var elementsOf = (part) => part.kind === "fade" ? part.els : [part.el];
131
+
132
+ class SectionEntranceController {
133
+ constructor(host, enabled) {
134
+ this.parts = [];
135
+ this.host = host;
136
+ this.enabled = enabled;
137
+ this.motion = new MotionController(host);
138
+ host.addController(this);
139
+ }
140
+ hostUpdated() {
141
+ if (this.observer || this.state !== undefined || !this.enabled())
142
+ return;
143
+ if (typeof IntersectionObserver === "undefined") {
144
+ this.state = "static";
145
+ return;
146
+ }
147
+ this.observer = new IntersectionObserver((entries) => {
148
+ const entry = entries[entries.length - 1];
149
+ if (entry)
150
+ this.report(entry.isIntersecting);
151
+ });
152
+ this.observer.observe(this.host);
153
+ }
154
+ hostDisconnected() {
155
+ this.stopObserving();
156
+ if (this.state === "held") {
157
+ this.parts.flatMap(elementsOf).forEach(releaseHidden);
158
+ this.state = undefined;
159
+ }
160
+ }
161
+ report(inView) {
162
+ if (this.state === undefined) {
163
+ const play = !inView && !prefersReducedMotion();
164
+ this.state = play ? "held" : "static";
165
+ if (!play)
166
+ return this.stopObserving();
167
+ this.parts = sectionEntranceParts(this.host);
168
+ this.parts.flatMap(elementsOf).forEach(holdHidden);
169
+ }
170
+ if (this.state === "held" && inView) {
171
+ this.state = "played";
172
+ this.stopObserving();
173
+ this.play();
174
+ }
175
+ }
176
+ play() {
177
+ this.parts.flatMap(elementsOf).forEach(releaseHidden);
178
+ const splits = [];
179
+ const steps = this.parts.map((part, index) => {
180
+ const overlap = index === 0 ? undefined : MOTION_OVERLAP;
181
+ if (part.kind === "fade") {
182
+ return { targets: part.els, preset: "fade", stagger: 0, overlap };
183
+ }
184
+ const split = splitText(part.el);
185
+ splits.push(split);
186
+ return {
187
+ targets: split.chars,
188
+ preset: "fade",
189
+ y: HEADING_RISE,
190
+ stagger: HEADING_STAGGER,
191
+ maxDuration: HEADING_MAX_SPREAD
192
+ };
193
+ });
194
+ if (steps.length === 0)
195
+ return;
196
+ this.motion.timeline(steps).finished.then(() => splits.forEach((split) => split.revert()));
197
+ }
198
+ stopObserving() {
199
+ this.observer?.disconnect();
200
+ this.observer = undefined;
201
+ }
202
+ }
203
+
204
+ // common/helpers/enum-validator.ts
205
+ var enumValidator = (propName, enumSet, candidateValue) => {
206
+ if (!enumSet.has(candidateValue)) {
207
+ throw new Error(`Invalid prop ${propName} of value ${candidateValue}. Expected one of: ${Array.from(enumSet).join(", ")}`);
208
+ }
209
+ };
210
+
211
+ // common/helpers/enum-creator.ts
212
+ var enumCreator = (acceptableValues, propName, type, other = {}) => {
213
+ const acceptableValuesSet = new Set(acceptableValues);
214
+ return {
215
+ type,
216
+ hasChanged(newValue, oldValue) {
217
+ if (newValue !== oldValue) {
218
+ enumValidator(propName, acceptableValuesSet, newValue);
219
+ return true;
220
+ }
221
+ return false;
222
+ },
223
+ ...other
224
+ };
225
+ };
226
+
227
+ // common/props/motion/motion.props.ts
228
+ var motionValues = {
229
+ ON: "on",
230
+ OFF: "off"
231
+ };
232
+ var motionProps = enumCreator(Object.values(motionValues), "motion", String, { reflect: true });
233
+
94
234
  // components/Container/src/Container.css
95
235
  import { css as css2 } from "lit";
96
236
  var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
@@ -138,6 +278,9 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
138
278
  .mx-auto {
139
279
  margin-inline: auto;
140
280
  }
281
+ .block {
282
+ display: block;
283
+ }
141
284
  .w-full {
142
285
  width: 100%;
143
286
  }
@@ -229,29 +372,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
229
372
  `;
230
373
  var Container_default = styles2;
231
374
 
232
- // common/helpers/enum-validator.ts
233
- var enumValidator = (propName, enumSet, candidateValue) => {
234
- if (!enumSet.has(candidateValue)) {
235
- throw new Error(`Invalid prop ${propName} of value ${candidateValue}. Expected one of: ${Array.from(enumSet).join(", ")}`);
236
- }
237
- };
238
-
239
- // common/helpers/enum-creator.ts
240
- var enumCreator = (acceptableValues, propName, type, other = {}) => {
241
- const acceptableValuesSet = new Set(acceptableValues);
242
- return {
243
- type,
244
- hasChanged(newValue, oldValue) {
245
- if (newValue !== oldValue) {
246
- enumValidator(propName, acceptableValuesSet, newValue);
247
- return true;
248
- }
249
- return false;
250
- },
251
- ...other
252
- };
253
- };
254
-
255
375
  // components/Container/src/Container.props.ts
256
376
  var containerSizeValues = {
257
377
  SM: "sm",
@@ -341,6 +461,8 @@ class Container extends Base {
341
461
  super(...arguments);
342
462
  this.size = containerSizeValues.LG;
343
463
  this.section = false;
464
+ this.motion = motionValues.OFF;
465
+ this._entrance = new SectionEntranceController(this, () => this.motion === motionValues.ON);
344
466
  }
345
467
  static styles = [Base.styles, Container_default];
346
468
  render() {
@@ -366,6 +488,9 @@ __legacyDecorateClassTS([
366
488
  __legacyDecorateClassTS([
367
489
  property({ ...paddingBottomProps })
368
490
  ], Container.prototype, "paddingBottom", undefined);
491
+ __legacyDecorateClassTS([
492
+ property({ ...motionProps })
493
+ ], Container.prototype, "motion", undefined);
369
494
  defineElement("dsgn-container", Container);
370
495
  export {
371
496
  Container
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-gn/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "UI components for the DS-GN Design System",
5
5
  "type": "module",
6
6
  "exports": {