@ds-gn/ui 0.8.0 → 0.9.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.
@@ -1520,6 +1520,15 @@
1520
1520
  "privacy": "private",
1521
1521
  "description": "The feature whose detail last faded in; the fade replays when it changes."
1522
1522
  },
1523
+ {
1524
+ "kind": "field",
1525
+ "name": "_motionState",
1526
+ "type": {
1527
+ "text": "'static' | 'held' | 'played' | undefined"
1528
+ },
1529
+ "privacy": "private",
1530
+ "description": "Motion progress, decided by the first visibility report."
1531
+ },
1523
1532
  {
1524
1533
  "kind": "field",
1525
1534
  "name": "surface",
@@ -1606,6 +1615,32 @@
1606
1615
  "description": "Accessible label for the tab list.",
1607
1616
  "attribute": "tabs-aria-label"
1608
1617
  },
1618
+ {
1619
+ "kind": "field",
1620
+ "name": "motion",
1621
+ "type": {
1622
+ "text": "MotionType"
1623
+ },
1624
+ "description": "Animate the section in when it scrolls into view: the header's heading\nletter by letter, then its description, then the tabs as a whole. The\neyebrow stays static. A section already in view when it loads, and\nreduced motion, stay static.",
1625
+ "attribute": "motion",
1626
+ "reflects": true
1627
+ },
1628
+ {
1629
+ "kind": "method",
1630
+ "name": "_updateMotion",
1631
+ "privacy": "private",
1632
+ "description": "Decide the motion from the first visibility report: a section already\non screen stays static; one off screen holds its parts hidden (from\nscript, so nothing is hidden when scripts do not run) and plays when it\ncomes into view, so even a jump to it shows no unanimated frame."
1633
+ },
1634
+ {
1635
+ "kind": "method",
1636
+ "name": "_motionParts",
1637
+ "privacy": "private"
1638
+ },
1639
+ {
1640
+ "kind": "method",
1641
+ "name": "_playMotion",
1642
+ "privacy": "private"
1643
+ },
1609
1644
  {
1610
1645
  "kind": "method",
1611
1646
  "name": "_fadeInDetail",
@@ -1862,6 +1897,14 @@
1862
1897
  },
1863
1898
  "default": "DEFAULT_TABS_ARIA_LABEL",
1864
1899
  "description": "Accessible label for the tab list."
1900
+ },
1901
+ {
1902
+ "name": "motion",
1903
+ "fieldName": "motion",
1904
+ "type": {
1905
+ "text": "MotionType"
1906
+ },
1907
+ "description": "Animate the section in when it scrolls into view: the header's heading\nletter by letter, then its description, then the tabs as a whole. The\neyebrow stays static. A section already in view when it loads, and\nreduced motion, stay static."
1865
1908
  }
1866
1909
  ]
1867
1910
  }
@@ -2642,6 +2685,32 @@
2642
2685
  "description": "Space below the section. Unset follows the inherited section space.",
2643
2686
  "attribute": "padding-bottom",
2644
2687
  "reflects": true
2688
+ },
2689
+ {
2690
+ "kind": "field",
2691
+ "name": "motion",
2692
+ "type": {
2693
+ "text": "MotionType"
2694
+ },
2695
+ "description": "Animate the content's eyebrow in letter by letter once the hero loads,\neach letter fading in with a small rise.\nEverything else stays static, so the hero's largest text paints at once.\nUnder reduced motion the eyebrow is static too.",
2696
+ "attribute": "motion",
2697
+ "reflects": true
2698
+ },
2699
+ {
2700
+ "kind": "field",
2701
+ "name": "_motion",
2702
+ "privacy": "private",
2703
+ "default": "new MotionController(this)"
2704
+ },
2705
+ {
2706
+ "kind": "method",
2707
+ "name": "_eyebrows",
2708
+ "privacy": "private",
2709
+ "return": {
2710
+ "type": {
2711
+ "text": "Element[]"
2712
+ }
2713
+ }
2645
2714
  }
2646
2715
  ],
2647
2716
  "superclass": {
@@ -2698,6 +2767,14 @@
2698
2767
  "text": "SectionSpaceType | undefined"
2699
2768
  },
2700
2769
  "description": "Space below the section. Unset follows the inherited section space."
2770
+ },
2771
+ {
2772
+ "name": "motion",
2773
+ "fieldName": "motion",
2774
+ "type": {
2775
+ "text": "MotionType"
2776
+ },
2777
+ "description": "Animate the content's eyebrow in letter by letter once the hero loads,\neach letter fading in with a small rise.\nEverything else stays static, so the hero's largest text paints at once.\nUnder reduced motion the eyebrow is static too."
2701
2778
  }
2702
2779
  ]
2703
2780
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether a section animates in. Components with their own motion
3
+ * vocabulary (Modal's direction values) declare their own `motion` values.
4
+ */
5
+ export declare const motionValues: {
6
+ readonly ON: "on";
7
+ readonly OFF: "off";
8
+ };
9
+ export declare const motionProps: import("lit").PropertyDeclaration<unknown, unknown>;
10
+ export type MotionType = (typeof motionValues)[keyof typeof motionValues];
@@ -8,6 +8,7 @@ import '../../Icon/src/Icon.js';
8
8
  import { Base } from '../../../common/Base/Base.js';
9
9
  import { type SurfaceType } from '../../../common/props/surface/surface.props.js';
10
10
  import { type CSSResultGroup } from 'lit';
11
+ import { type MotionType } from '../../../common/props/motion/motion.props.js';
11
12
  import { type SectionSpaceType } from '../../Container/src/Container.props.js';
12
13
  import { type FeatureTabData } from './FeatureTabs.props.js';
13
14
  /** @slot header - Optional content above the tab interface. */
@@ -22,6 +23,8 @@ export declare class FeatureTabs extends Base {
22
23
  private _motion;
23
24
  /** The feature whose detail last faded in; the fade replays when it changes. */
24
25
  private _shownIndex?;
26
+ /** Motion progress, decided by the first visibility report. */
27
+ private _motionState?;
25
28
  surface: SurfaceType;
26
29
  features: FeatureTabData[];
27
30
  duration: number;
@@ -37,8 +40,24 @@ export declare class FeatureTabs extends Base {
37
40
  resumeAriaLabel: string;
38
41
  /** Accessible label for the tab list. */
39
42
  tabsAriaLabel: string;
43
+ /**
44
+ * Animate the section in when it scrolls into view: the header's heading
45
+ * letter by letter, then its description, then the tabs as a whole. The
46
+ * eyebrow stays static. A section already in view when it loads, and
47
+ * reduced motion, stay static.
48
+ */
49
+ motion: MotionType;
40
50
  constructor();
41
51
  firstUpdated(): void;
52
+ /**
53
+ * Decide the motion from the first visibility report: a section already
54
+ * on screen stays static; one off screen holds its parts hidden (from
55
+ * script, so nothing is hidden when scripts do not run) and plays when it
56
+ * comes into view, so even a jump to it shows no unanimated frame.
57
+ */
58
+ private _updateMotion;
59
+ private _motionParts;
60
+ private _playMotion;
42
61
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
43
62
  /**
44
63
  * Fade the detail in whenever the active feature changes. `keyed` renders a
@@ -218,6 +218,10 @@ var surfaceValues = {
218
218
  var surfaceProps = enumCreator(Object.values(surfaceValues), "surface", String, { reflect: true });
219
219
 
220
220
  // components/FeatureTabs/src/FeatureTabs.ts
221
+ import {
222
+ prefersReducedMotion as prefersReducedMotion2,
223
+ splitText
224
+ } from "@ds-gn/motion";
221
225
  import { MotionController } from "@ds-gn/motion/lit";
222
226
 
223
227
  // ../../node_modules/.bun/lit-html@3.3.3/node_modules/lit-html/development/is-server.js
@@ -294,6 +298,15 @@ import {
294
298
  import { property } from "lit/decorators.js";
295
299
  import { keyed } from "lit/directives/keyed.js";
296
300
  import { styleMap } from "lit/directives/style-map.js";
301
+
302
+ // common/props/motion/motion.props.ts
303
+ var motionValues = {
304
+ ON: "on",
305
+ OFF: "off"
306
+ };
307
+ var motionProps = enumCreator(Object.values(motionValues), "motion", String, { reflect: true });
308
+
309
+ // components/FeatureTabs/src/FeatureTabs.ts
297
310
  import {
298
311
  containerSizeValues,
299
312
  paddingBottomProps,
@@ -1003,13 +1016,20 @@ var dotPipInnerClassNames = (active) => clsx_default("block", "rounded-full", "h
1003
1016
  var playPauseClassNames = () => clsx_default("appearance-none", "border-0", "outline-none", "bg-transparent", "cursor-pointer", "text-accent", "py-[10px]", "px-[8px]", "flex", "items-center", "justify-center", "rounded-full", "transition-colors", "duration-150");
1004
1017
 
1005
1018
  // components/FeatureTabs/src/FeatureTabs.ts
1019
+ var holdHidden = (el) => el.style.setProperty("opacity", "0");
1020
+ var releaseHidden = (el) => el.style.removeProperty("opacity");
1021
+ var HEADING_STAGGER = 20;
1022
+ var HEADING_MAX_SPREAD = 500;
1023
+ var HEADING_RISE = 4;
1024
+ var MOTION_OVERLAP = 160;
1006
1025
  var FEATURE_TAB_ICONS = {
1007
1026
  terminal: svg`<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>`,
1008
1027
  brain: svg`<path d="M9 4.5a2.5 2.5 0 0 0-2.5 2.5v.5a2.5 2.5 0 0 0-2 4 2.5 2.5 0 0 0 2 4v.5A2.5 2.5 0 0 0 9 18.5a2.5 2.5 0 0 0 3-2.5V6a2.5 2.5 0 0 0-3-1.5z"/><path d="M15 4.5A2.5 2.5 0 0 1 17.5 7v.5a2.5 2.5 0 0 1 2 4 2.5 2.5 0 0 1-2 4v.5A2.5 2.5 0 0 1 15 18.5a2.5 2.5 0 0 1-3-2.5V6a2.5 2.5 0 0 1 3-1.5z"/>`,
1009
1028
  cpu: svg`<rect x="5" y="5" width="14" height="14" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="2" x2="9" y2="5"/><line x1="15" y1="2" x2="15" y2="5"/><line x1="9" y1="19" x2="9" y2="22"/><line x1="15" y1="19" x2="15" y2="22"/><line x1="2" y1="9" x2="5" y2="9"/><line x1="2" y1="15" x2="5" y2="15"/><line x1="19" y1="9" x2="22" y2="9"/><line x1="19" y1="15" x2="22" y2="15"/>`,
1010
1029
  palette: svg`<circle cx="12" cy="12" r="9"/><circle cx="7.5" cy="10.5" r="1" fill="currentColor"/><circle cx="12" cy="7.5" r="1" fill="currentColor"/><circle cx="16.5" cy="10.5" r="1" fill="currentColor"/><path d="M12 21a3 3 0 0 1-3-3c0-1.5 1-2 1-3a2 2 0 0 0-2-2"/>`,
1011
1030
  plug: svg`<path d="M9 2v6"/><path d="M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-5 5 5 5 0 0 1-5-5z"/><path d="M12 16v6"/>`,
1012
- shield: svg`<path d="M12 3l8 3v5c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/>`
1031
+ shield: svg`<path d="M12 3l8 3v5c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/>`,
1032
+ zap: svg`<path d="M13 2 4 14h7l-1 8 9-12h-7z"/>`
1013
1033
  };
1014
1034
  var nextTabIndex = (key, index, lastIndex) => {
1015
1035
  switch (key) {
@@ -1046,11 +1066,67 @@ class FeatureTabs extends Base {
1046
1066
  this.pauseAriaLabel = DEFAULT_PAUSE_ARIA_LABEL;
1047
1067
  this.resumeAriaLabel = DEFAULT_RESUME_ARIA_LABEL;
1048
1068
  this.tabsAriaLabel = DEFAULT_TABS_ARIA_LABEL;
1069
+ this.motion = motionValues.OFF;
1049
1070
  this._rotate = new AutoRotateController(this, DEFAULT_DURATION, () => this._inView.value === true);
1050
1071
  }
1051
1072
  firstUpdated() {
1052
1073
  this._rotate.resume(this.features.length);
1053
1074
  }
1075
+ _updateMotion() {
1076
+ const inView = this._inView.value;
1077
+ if (inView === undefined)
1078
+ return;
1079
+ if (this._motionState === undefined) {
1080
+ const play = this.motion === motionValues.ON && !inView && !prefersReducedMotion2();
1081
+ this._motionState = play ? "held" : "static";
1082
+ if (play)
1083
+ this._motionParts().forEach(holdHidden);
1084
+ }
1085
+ if (this._motionState === "held" && inView) {
1086
+ this._motionState = "played";
1087
+ this._playMotion();
1088
+ }
1089
+ }
1090
+ _motionParts() {
1091
+ const header = Array.from(this.children).filter((el) => el.slot === "header");
1092
+ const find = (slot) => header.map((el) => el.querySelector(`[slot="${slot}"]`)).find((el) => el !== null);
1093
+ return [
1094
+ find("heading"),
1095
+ find("description"),
1096
+ ...this.renderRoot.querySelectorAll("[data-motion-body]")
1097
+ ].filter((el) => el !== undefined);
1098
+ }
1099
+ _playMotion() {
1100
+ const parts = this._motionParts();
1101
+ parts.forEach(releaseHidden);
1102
+ const heading = parts.find((el) => el.slot === "heading");
1103
+ const description = parts.find((el) => el.slot === "description");
1104
+ const split = heading ? splitText(heading) : undefined;
1105
+ const steps = [
1106
+ ...split ? [
1107
+ {
1108
+ targets: split.chars,
1109
+ preset: "fade",
1110
+ y: HEADING_RISE,
1111
+ stagger: HEADING_STAGGER,
1112
+ maxDuration: HEADING_MAX_SPREAD
1113
+ }
1114
+ ] : [],
1115
+ ...description ? [
1116
+ {
1117
+ el: description,
1118
+ preset: "fade",
1119
+ overlap: MOTION_OVERLAP
1120
+ }
1121
+ ] : [],
1122
+ {
1123
+ targets: parts.filter((el) => el.hasAttribute("data-motion-body")),
1124
+ preset: "fade",
1125
+ overlap: MOTION_OVERLAP
1126
+ }
1127
+ ];
1128
+ this._motion.timeline(steps).finished.then(() => split?.revert());
1129
+ }
1054
1130
  updated(changedProperties) {
1055
1131
  super.updated(changedProperties);
1056
1132
  this.updateTheme(changedProperties, this.surface);
@@ -1058,6 +1134,7 @@ class FeatureTabs extends Base {
1058
1134
  this._rotate.setDuration(this.duration);
1059
1135
  }
1060
1136
  this._fadeInDetail();
1137
+ this._updateMotion();
1061
1138
  }
1062
1139
  _fadeInDetail() {
1063
1140
  const index = this._rotate.activeIndex;
@@ -1262,12 +1339,12 @@ class FeatureTabs extends Base {
1262
1339
  </div>
1263
1340
 
1264
1341
  <!-- Mobile: detail panel only, no grid -->
1265
- <div class="lg:hidden">
1342
+ <div class="lg:hidden" data-motion-body>
1266
1343
  ${this._renderDetailPanel(idx, activeFeature, progress, total, paused, features)}
1267
1344
  </div>
1268
1345
 
1269
1346
  <!-- Desktop: full grid with tabs + detail -->
1270
- <div class="hidden lg:block">
1347
+ <div class="hidden lg:block" data-motion-body>
1271
1348
  <dsgn-grid cols="7-5" gap="xl" ?reverse=${true}>
1272
1349
  ${this._renderTabList(features, idx, progress, paused, "col-2")}
1273
1350
  ${this._renderDetailPanel(idx, activeFeature, progress, total, paused, features, "col-1")}
@@ -1311,6 +1388,9 @@ __legacyDecorateClassTS([
1311
1388
  __legacyDecorateClassTS([
1312
1389
  property({ type: String, attribute: "tabs-aria-label" })
1313
1390
  ], FeatureTabs.prototype, "tabsAriaLabel", undefined);
1391
+ __legacyDecorateClassTS([
1392
+ property({ ...motionProps })
1393
+ ], FeatureTabs.prototype, "motion", undefined);
1314
1394
  defineElement("dsgn-feature-tabs", FeatureTabs);
1315
1395
  export {
1316
1396
  FeatureTabs
@@ -1,4 +1,4 @@
1
- export type FeatureTabIcon = 'terminal' | 'brain' | 'cpu' | 'palette' | 'plug' | 'shield';
1
+ export type FeatureTabIcon = 'terminal' | 'brain' | 'cpu' | 'palette' | 'plug' | 'shield' | 'zap';
2
2
  export interface FeatureTabData {
3
3
  id: string;
4
4
  eyebrow: string;
@@ -5,6 +5,7 @@ import '../../Grid/src/Grid.js';
5
5
  import '../../HeadingBlock/src/HeadingBlock.js';
6
6
  import { type CSSResultGroup } from 'lit';
7
7
  import { Base } from '../../../common/Base/Base.js';
8
+ import { type MotionType } from '../../../common/props/motion/motion.props.js';
8
9
  import { type SurfaceType } from '../../../common/props/surface/surface.props.js';
9
10
  import { type SectionSpaceType } from '../../Container/src/Container.props.js';
10
11
  import { type HeroContentAlignType, type HeroLayoutType, type HeroMinHeightType } from './Hero.props.js';
@@ -37,6 +38,16 @@ export declare class Hero extends Base {
37
38
  paddingTop?: SectionSpaceType;
38
39
  /** Space below the section. Unset follows the inherited section space. */
39
40
  paddingBottom?: SectionSpaceType;
41
+ /**
42
+ * Animate the content's eyebrow in letter by letter once the hero loads,
43
+ * each letter fading in with a small rise.
44
+ * Everything else stays static, so the hero's largest text paints at once.
45
+ * Under reduced motion the eyebrow is static too.
46
+ */
47
+ motion: MotionType;
48
+ private _motion;
49
+ firstUpdated(): void;
50
+ private _eyebrows;
40
51
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
41
52
  render(): import("lit").TemplateResult<1>;
42
53
  }
@@ -14,6 +14,8 @@ import"../../Container/src/Container.js";
14
14
  import"../../FormBlock/src/FormBlock.js";
15
15
  import"../../Grid/src/Grid.js";
16
16
  import"../../HeadingBlock/src/HeadingBlock.js";
17
+ import { prefersReducedMotion, splitText } from "@ds-gn/motion";
18
+ import { MotionController } from "@ds-gn/motion/lit";
17
19
  import { html as html2 } from "lit";
18
20
  import { property } from "lit/decorators.js";
19
21
 
@@ -118,6 +120,13 @@ var enumCreator = (acceptableValues, propName, type, other = {}) => {
118
120
  };
119
121
  };
120
122
 
123
+ // common/props/motion/motion.props.ts
124
+ var motionValues = {
125
+ ON: "on",
126
+ OFF: "off"
127
+ };
128
+ var motionProps = enumCreator(Object.values(motionValues), "motion", String, { reflect: true });
129
+
121
130
  // common/props/surface/surface.props.ts
122
131
  var surfaceValues = {
123
132
  LIGHT: "light",
@@ -189,6 +198,9 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
189
198
  .grid {
190
199
  display: grid;
191
200
  }
201
+ .hidden {
202
+ display: none;
203
+ }
192
204
  .h-64 {
193
205
  height: calc(var(--spacing) * 64);
194
206
  }
@@ -412,6 +424,11 @@ var centeredFormClassNames = (contentAlign) => {
412
424
  };
413
425
 
414
426
  // components/Hero/src/Hero.ts
427
+ var EYEBROW_DELAY = 500;
428
+ var EYEBROW_STAGGER = 30;
429
+ var EYEBROW_MAX_SPREAD = 600;
430
+ var EYEBROW_RISE = 4;
431
+
415
432
  class Hero extends Base {
416
433
  constructor() {
417
434
  super(...arguments);
@@ -419,8 +436,27 @@ class Hero extends Base {
419
436
  this.layout = heroLayoutValues.ASSET_RIGHT;
420
437
  this.minHeight = heroMinHeightValues.MD;
421
438
  this.contentAlign = heroContentAlignValues.START;
439
+ this.motion = motionValues.OFF;
440
+ this._motion = new MotionController(this);
422
441
  }
423
442
  static styles = [Base.styles, Hero_default];
443
+ firstUpdated() {
444
+ if (this.motion !== motionValues.ON || prefersReducedMotion())
445
+ return;
446
+ this._eyebrows().forEach((eyebrow) => {
447
+ const split = splitText(eyebrow);
448
+ this._motion.sequence(split.chars, {
449
+ preset: "fade",
450
+ y: EYEBROW_RISE,
451
+ delay: EYEBROW_DELAY,
452
+ stagger: EYEBROW_STAGGER,
453
+ maxDuration: EYEBROW_MAX_SPREAD
454
+ }).finished.then(split.revert);
455
+ });
456
+ }
457
+ _eyebrows() {
458
+ return Array.from(this.children).filter((el) => el.slot === "content").flatMap((el) => Array.from(el.querySelectorAll('[slot="eyebrow"]')));
459
+ }
424
460
  updated(changedProperties) {
425
461
  super.updated(changedProperties);
426
462
  this.updateTheme(changedProperties, this.surface);
@@ -526,6 +562,9 @@ __legacyDecorateClassTS([
526
562
  __legacyDecorateClassTS([
527
563
  property({ ...paddingBottomProps })
528
564
  ], Hero.prototype, "paddingBottom", undefined);
565
+ __legacyDecorateClassTS([
566
+ property({ ...motionProps })
567
+ ], Hero.prototype, "motion", undefined);
529
568
  defineElement("dsgn-hero", Hero);
530
569
  export {
531
570
  Hero
@@ -489,6 +489,11 @@ dsgn-hero:not(:defined)[layout='asset-background']:not(:has(> [slot='features'])
489
489
  padding-bottom: calc(var(--spacing) * 6);
490
490
  grid-row: 3;
491
491
  }
492
+ @media (prefers-reduced-motion: no-preference) {
493
+ dsgn-hero:not(:defined)[motion='on'] > [slot='content'] [slot='eyebrow'] {
494
+ opacity: 0;
495
+ }
496
+ }
492
497
  @property --tw-tracking {
493
498
  syntax: "*";
494
499
  inherits: false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-gn/ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "UI components for the DS-GN Design System",
5
5
  "type": "module",
6
6
  "exports": {
@@ -346,7 +346,7 @@
346
346
  "react-dom": "19.2.4"
347
347
  },
348
348
  "dependencies": {
349
- "@ds-gn/motion": "0.8.0",
349
+ "@ds-gn/motion": "0.9.0",
350
350
  "@ds-gn/tokens": "0.8.0",
351
351
  "@lit-labs/observers": "^2.0.5",
352
352
  "@lit/react": "^1.0.7",