mosaic-headless 1.3.0 → 1.5.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.
@@ -68,16 +68,61 @@ Confirmed from it:
68
68
  it is a percentage of the scroll range, not milliseconds.
69
69
  - **`easing` is defaulted to `ease`** by the server.
70
70
 
71
- ## What is NOT solved
71
+ ## What is NOT solved - and exactly how far it now goes
72
72
 
73
- **Property binding.** `propertyMetas` and per-keyframe `properties` did not survive
74
- into the payload in any shape tried: flat array of metas, metas nested one level
75
- deeper (which made the whole interaction vanish), properties as an object, properties
76
- as an array. The keyframes arrive with timing but with nothing to animate.
73
+ `tools/sweep_interactions.py` commits candidate shapes to a live page and judges each
74
+ against `var mosaicInteractions` in the delivered HTML **and** against the row read
75
+ back out of the database, with negative controls. Results in
76
+ `data/interaction-verification.csv`.
77
77
 
78
- Do not ship an interaction animation on the strength of this page. Use the CSS path,
79
- or drive the editor once by hand and read the stored row back out of `wp_mosaic_nodes`
80
- — that row is the authoritative example, and one look at it would settle the shape.
78
+ **`propertyMetas` is NOT the problem, and the previous claim here was wrong.** It is
79
+ accepted, stored, and keeps its order:
80
+
81
+ ```json
82
+ "propertyMetas": [{"uuid": "<uuid>", "type": "predefined", "timelineKey": "_",
83
+ "predefinedOptions": {"name": "opacity"}}]
84
+ ```
85
+
86
+ `type` is one of `predefined | custom | static | collectionMode | collectionVariable
87
+ | display`; the options key is that type plus `Options`
88
+ (`KeyframePropertyMetaTypeFactoryAbstract::getPropertyMetaOptionsDataName()`), and
89
+ for `predefined` the `name` is the property itself - the full list is
90
+ `data/animatable-properties.csv`.
91
+
92
+ **`uuid` is optional on create and REQUIRED on update.** `DataArray::initValues`
93
+ mints one when it is missing, but `DataArray::applyCommitData` matches existing items
94
+ by uuid and rebuilds `order` from `array_column($new, 'uuid')` - so on any later
95
+ commit an item without a uuid is not matched, not carried into the new order, and
96
+ disappears. Measured both ways: the same shape stored 1 meta on create and 0 after an
97
+ update.
98
+
99
+ **What still does not bind is `initial` and the keyframes' `properties`.** Four
100
+ attempts, each with the row read back:
101
+
102
+ | attempt | result |
103
+ |---|---|
104
+ | one commit carrying metas + values | metas stored, 0 property values |
105
+ | second commit adding the values | metas stored, 0 property values |
106
+ | second commit that also REORDERS the metas, so `propertyMetas/order` genuinely changes | reorder took effect in storage, still 0 property values |
107
+ | the values written **straight into `wp_mosaic_nodes`**, all caches flushed | still absent from the payload |
108
+
109
+ That last row is the informative one: it rules out the trust model, the commit
110
+ validators and the cache. The gate is in the export itself -
111
+ `KeyframePropertiesDataSub::exportForInteraction()` will only emit a property for
112
+ which `$this->hasSubData($name)` is true, and those descriptors exist only where
113
+ `AnimationActionOptionsDataSubAbstract::syncAttachedPropertyMetas()` has created
114
+ them, on a shared `DataMeta` that starts empty every time the object is constructed.
115
+
116
+ So the honest position is: **the envelope, the trigger, the timeline and the property
117
+ metas can all be written headlessly; the property VALUES have not been.** Use the CSS
118
+ path, which is fully verified - see `data/style-state-verification.csv`, where 36 of
119
+ 37 probed states compiled to exactly the selector the table promises.
120
+
121
+ The one experiment left, and it is cheap: author a single interaction in the editor,
122
+ then `SELECT data FROM wp_mosaic_nodes` for that node. One look settles it.
123
+
124
+ **`pointerEnter` produced no `mosaicInteractions` entry at all**, so the timed family
125
+ needs something the progress family does not - also unfinished.
81
126
 
82
127
  ## The failure mode that makes this hard
83
128
 
package/sites/_moksa.py CHANGED
@@ -72,10 +72,22 @@ def T(tag, text, _t=None, _m=None, **st):
72
72
  "style": bp(st, _t, _m) if (st or _t or _m) else None}
73
73
 
74
74
 
75
- def box(attr, style, children, hover=None, _t=None, _m=None):
75
+ # The keyboard is a state too.
76
+ #
77
+ # `focus-visible` was verified by sweep_style_states.py to compile to
78
+ # `.M_EL<n>:FOCUS-VISIBLE` - note the UPPERCASE pseudo-class Mosaic emits, which is
79
+ # why grepping a delivered stylesheet for ":focus-visible" finds nothing at all. Of
80
+ # the seven globally usable states this page used exactly one, `hover`, which means
81
+ # every route through it was invisible to anyone not using a mouse.
82
+ FOCUS_RING = {"customStyles": "outline:2px solid rgb(255,90,54);outline-offset:3px;"}
83
+
84
+
85
+ def box(attr, style, children, hover=None, focus=None, _t=None, _m=None):
76
86
  s = bp(style, _t, _m) or {"&": {"_": {}}}
77
87
  if hover:
78
88
  s["hover"] = {"_": hover}
89
+ if focus:
90
+ s["focus-visible"] = {"_": focus}
79
91
  return {"type": "div", "data": {"attrID": attr}, "style": s, "children": children}
80
92
 
81
93
 
@@ -364,6 +376,10 @@ def motion_css():
364
376
  for i, (_g, items) in enumerate(STACK)
365
377
  for j in range(len(items)))
366
378
  marks = ",".join("#mk-mark-%d" % i for i in range(4))
379
+ # the boot list lights one clause at a time
380
+ boot_stagger = "\n".join(
381
+ " #mk-boot-c%d{animation-delay:%dms}" % (i, 260 + i * 120)
382
+ for i in range(len(CLAUSES)))
367
383
  # a named view timeline per section, declared on the section and consumed by its
368
384
  # index entry - `timeline-scope` on #mk-doc is what lets the name cross between
369
385
  # two elements that are not ancestor and descendant
@@ -516,21 +532,94 @@ def motion_css():
516
532
  "@media (max-width:1079px){#mk-cue{right:28px}}",
517
533
  "@media (max-width:767px){#mk-cue{right:18px;bottom:16px}}",
518
534
 
535
+ # ── the entrance sequence ────────────────────────────────────────────
536
+ # A page-load animation has one catastrophic failure mode: an overlay that
537
+ # covers the document and never leaves. So the veil is `display:none` in the
538
+ # BASE rule and is only switched on inside the motion query, alongside the
539
+ # animation that removes it. If motion is reduced, or the query never matches,
540
+ # or the stylesheet is truncated, the overlay does not exist at all - the
541
+ # failure direction is "no intro", never "no page".
542
+ "#mk-boot{display:none}",
543
+ # An integer that can be animated, and read back out of the element - which
544
+ # is what makes the counter verifiable rather than merely visible.
545
+ # `inherits:true` is load-bearing, and the reason is not obvious. The
546
+ # animation runs on #mk-boot-num, but the digits are drawn by its ::after
547
+ # through `counter(n)`. With `inherits:false` the pseudo-element never sees
548
+ # the animated value: measured mid-run, --mk-n was 18 on the element and 0
549
+ # on its ::after, so the counter sat at 0 for the whole sequence while the
550
+ # property underneath it animated perfectly.
551
+ "@property --mk-n{syntax:'<integer>';initial-value:0;inherits:true}",
552
+ "@keyframes mk-count{to{--mk-n:100}}",
553
+ "@keyframes mk-boot-out{"
554
+ "0%{clip-path:inset(0 0 0 0)}"
555
+ "100%{clip-path:inset(0 0 100% 0);visibility:hidden}}",
556
+ "@keyframes mk-boot-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}",
557
+ "@keyframes mk-boot-line{from{opacity:.14}"
558
+ "60%{opacity:1;color:rgb(255,90,54)}to{opacity:.55;color:rgb(107,109,113)}}",
559
+ "@keyframes mk-boot-scan{from{transform:translateY(0)}"
560
+ "to{transform:translateY(100vh)}}",
561
+ "@keyframes mk-boot-fade{from{opacity:0}to{opacity:1}}",
562
+
519
563
  # ── motion, all of it opt-out-able ───────────────────────────────────
520
564
  "@media (prefers-reduced-motion:no-preference){",
565
+ " #mk-boot{display:grid;position:fixed;inset:0;z-index:999;"
566
+ "background:rgb(250,250,247);align-content:center;justify-items:center;"
567
+ "row-gap:22px;overflow:hidden;"
568
+ "animation:mk-boot-out .5s cubic-bezier(.7,0,.3,1) 1.55s forwards}",
569
+ " #mk-boot-head,#mk-boot-foot{position:absolute;left:40px;right:40px;"
570
+ "display:flex;justify-content:space-between;font-family:" + MONO + ";"
571
+ "font-size:10px;letter-spacing:.2em;color:rgb(107,109,113);"
572
+ "animation:mk-boot-fade .4s ease both}",
573
+ " #mk-boot-head{top:34px}",
574
+ " #mk-boot-foot{bottom:30px;animation-delay:.1s}",
575
+ " #mk-boot-num{font-family:" + MONO + ";font-size:96px;font-weight:500;"
576
+ "line-height:1;letter-spacing:-.04em;color:rgb(22,24,28);"
577
+ "font-variant-numeric:tabular-nums;min-width:3ch;text-align:right;"
578
+ "animation:mk-count 1.2s cubic-bezier(.3,0,0,1) .12s both}",
579
+ ' #mk-boot-num::after{counter-reset:n var(--mk-n);content:counter(n)}',
580
+ " #mk-boot-row{display:flex;align-items:flex-end;column-gap:8px;"
581
+ "line-height:1}",
582
+ # the darkened cut, not the fill orange: 18px of rgb(255,90,54) on paper is
583
+ # 2.97:1 and the design audit refuses it. The progress rule beside it keeps
584
+ # the bright colour, because a 1px hairline is not text.
585
+ " #mk-boot-pct{font-family:" + MONO + ";font-size:18px;"
586
+ "color:rgb(191,68,40);padding-bottom:12px}",
587
+ " #mk-boot-track{width:min(420px,62vw);height:1px;"
588
+ "background:rgba(22,24,28,.14);position:relative}",
589
+ " #mk-boot-fill{position:absolute;inset:0;background:rgb(255,90,54);"
590
+ "transform-origin:0 50%;"
591
+ "animation:mk-boot-fill 1.2s cubic-bezier(.3,0,0,1) .12s both}",
592
+ " #mk-boot-list{display:flex;column-gap:18px;row-gap:8px;"
593
+ "flex-wrap:wrap;justify-content:center;font-family:" + MONO + ";"
594
+ "font-size:10px;letter-spacing:.18em}",
595
+ " #mk-boot-list>*{opacity:.14;"
596
+ "animation:mk-boot-line .5s ease both}",
597
+ boot_stagger,
598
+ " #mk-boot::before{content:\"\";position:absolute;inset:0;"
599
+ "background:"
600
+ "repeating-linear-gradient(90deg,rgba(22,24,28,.045) 0 1px,"
601
+ "transparent 1px 72px),"
602
+ "repeating-linear-gradient(0deg,rgba(22,24,28,.045) 0 1px,"
603
+ "transparent 1px 72px);"
604
+ "animation:mk-boot-fade .5s ease both}",
605
+ " #mk-boot-scan{position:absolute;left:0;right:0;top:0;height:1px;"
606
+ "background:linear-gradient(90deg,rgba(255,90,54,0),rgba(255,90,54,.55),"
607
+ "rgba(255,90,54,0));animation:mk-boot-scan 1.6s linear .1s both}",
608
+ " @media (max-width:767px){#mk-boot-num{font-size:62px}"
609
+ "#mk-boot-head,#mk-boot-foot{left:18px;right:18px}}",
521
610
  " #mk-hl1-mask>*,#mk-hl2-mask>*{transform:translateY(112%);"
522
611
  "animation:mk-linein .95s cubic-bezier(.16,1,.3,1) forwards}",
523
- " #mk-hl1-mask>*{animation-delay:.2s}",
524
- " #mk-hl2-mask>*{animation-delay:.32s}",
612
+ " #mk-hl1-mask>*{animation-delay:1.72s}",
613
+ " #mk-hl2-mask>*{animation-delay:1.84s}",
525
614
  " #mk-mast-meta,#mk-mast-lede,#mk-mast-cta{opacity:0;"
526
615
  "animation:mk-softin .8s cubic-bezier(.16,1,.3,1) forwards}",
527
- " #mk-mast-meta{animation-delay:.06s}",
528
- " #mk-mast-lede{animation-delay:.66s}",
529
- " #mk-mast-cta{animation-delay:.78s}",
616
+ " #mk-mast-meta{animation-delay:1.62s}",
617
+ " #mk-mast-lede{animation-delay:2.10s}",
618
+ " #mk-mast-cta{animation-delay:2.22s}",
530
619
  " #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;"
531
- "animation-delay:.95s}",
620
+ "animation-delay:2.4s}",
532
621
  " #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
533
- "animation-delay:.44s}",
622
+ "animation-delay:1.96s}",
534
623
  " #mk-spec-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
535
624
  "animation-delay:.9s}",
536
625
  " #mk-cue{opacity:0;animation:mk-softin .8s ease forwards;"
@@ -632,7 +721,8 @@ HEADER = box("mk-shell-top", {}, [
632
721
  "transitionAll": "160ms ease",
633
722
  "cursor": "pointer"},
634
723
  "_m": {"fontSize": "12px"}},
635
- "hover": {"_": {"color": {"token": "--mk-accent"}}}},
724
+ "hover": {"_": {"color": {"token": "--mk-accent"}}},
725
+ "focus-visible": {"_": FOCUS_RING}},
636
726
  "text": text}
637
727
  for i, (text, href) in enumerate(NAV)
638
728
  ]},
@@ -656,7 +746,8 @@ HEADER = box("mk-shell-top", {}, [
656
746
  "color": "rgb(255,90,54)"},
657
747
  "transitionAll": "180ms ease"}},
658
748
  "hover": {"_": {"backgroundColor": {"token": "--mk-accent"},
659
- "color": {"token": "--mk-ink"}}}},
749
+ "color": {"token": "--mk-ink"}}},
750
+ "focus-visible": {"_": FOCUS_RING}},
660
751
  "text": "START A PROJECT"},
661
752
  ]}
662
753
  ])]),
@@ -790,6 +881,7 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
790
881
  "border-bottom-color:rgb(22,24,28);"},
791
882
  fontSize="13px", fontFamily=MONO, letterSpacing="0.1em",
792
883
  radius="0px", cursor="pointer", transitionAll="180ms ease")},
884
+ "focus-visible": {"_": FOCUS_RING},
793
885
  "hover": {"_": ({"backgroundColor": {"token": "--mk-ink"},
794
886
  "customStyles": "border:1px solid rgb(22,24,28);"}
795
887
  if n == 1
@@ -937,7 +1029,15 @@ STACK_SEC = section("stack", [wrap("mk-stack-in", [
937
1029
  {"display": "grid", "gridCols": "160px 1fr", "columnGap": "24px",
938
1030
  "alignItems": "baseline",
939
1031
  "paddingTop": "18px", "paddingBottom": "18px",
940
- "customStyles": "border-top:1px solid " + RULE + ";"},
1032
+ # Each row arrives on its own, staggered by pushing its
1033
+ # animation-range further down the scroll rather than by a
1034
+ # delay: a scroll-driven animation has no clock to delay
1035
+ # against, so the offset has to live in the range.
1036
+ "customStyles":
1037
+ "border-top:1px solid " + RULE + ";"
1038
+ "animation:mk-rise both;animation-timeline:view();"
1039
+ "animation-range:entry %d%% cover %d%%;"
1040
+ % (2 + i * 4, 18 + i * 4)},
941
1041
  _m={"gridCols": "1fr", "rowGap": "10px",
942
1042
  "paddingTop": "16px", "paddingBottom": "16px"},
943
1043
  children=[
@@ -1161,6 +1261,40 @@ CONTACT = section("contact", [wrap("mk-contact-in", [
1161
1261
  ]),
1162
1262
  ])])
1163
1263
 
1264
+ # ── the entrance sequence ─────────────────────────────────────────────────────
1265
+ # A boot screen, because the page is a specification document and this is what one
1266
+ # looks like while it is being read off a machine. It is CSS only: a counter that is
1267
+ # a real animated integer (`@property --mk-n`), a rule that fills with it, the seven
1268
+ # clauses reporting in one at a time, and a scan line. Then the whole panel clips
1269
+ # upward and is gone.
1270
+ #
1271
+ # Every id here is deliberate. `verify_intro.py` samples them over the first seconds
1272
+ # of the page's life, which is the only window in which any of this exists.
1273
+ BOOT = box("mk-boot", {}, [
1274
+ box("mk-boot-scan", {}, []),
1275
+ box("mk-boot-head", {}, [
1276
+ mono("MOKSA WEB — STUDIO PROFILE", size="10px", color="--mk-faint",
1277
+ track="0.2em"),
1278
+ mono("REV. 2026.09", size="10px", color="--mk-faint", track="0.2em"),
1279
+ ]),
1280
+ box("mk-boot-row", {}, [
1281
+ box("mk-boot-num", {}, []),
1282
+ box("mk-boot-pct", {}, [T("p", "%")]),
1283
+ ]),
1284
+ box("mk-boot-track", {}, [box("mk-boot-fill", {}, [])]),
1285
+ box("mk-boot-list", {},
1286
+ # each clause gets its own box because the id is what the stagger
1287
+ # targets, and `mono()` puts everything it is given into the style
1288
+ [box("mk-boot-c%d" % i, {},
1289
+ [mono("§" + num + " " + name, size="10px", color="--mk-faint",
1290
+ track="0.18em")])
1291
+ for i, (num, name, _h, _t) in enumerate(CLAUSES)]),
1292
+ box("mk-boot-foot", {}, [
1293
+ mono("INITIALISING", size="10px", color="--mk-faint", track="0.2em"),
1294
+ mono("TAICHUNG, TW", size="10px", color="--mk-faint", track="0.2em"),
1295
+ ]),
1296
+ ])
1297
+
1164
1298
  # Registration marks. A spec sheet is a printed object and these are how one is
1165
1299
  # trimmed; here they simply say the page has edges that were decided.
1166
1300
  MARKS = box("mk-marks", {}, [box("mk-mark-%d" % i, {}, []) for i in range(4)])
@@ -1176,7 +1310,8 @@ INDEX = box("mk-index", {}, [
1176
1310
  "fontFamily": MONO, "fontSize": "10px",
1177
1311
  "letterSpacing": "0.14em",
1178
1312
  "transitionAll": "220ms ease"}},
1179
- "hover": {"_": {"color": {"token": "--mk-ink"}}}},
1313
+ "hover": {"_": {"color": {"token": "--mk-ink"}}},
1314
+ "focus-visible": {"_": FOCUS_RING}},
1180
1315
  "text": "§" + num + " " + name}
1181
1316
  for i, (num, name, href, _tl) in enumerate(CLAUSES)
1182
1317
  ])
@@ -1187,7 +1322,7 @@ CUE = box("mk-cue", {}, [
1187
1322
  ])
1188
1323
 
1189
1324
  HOME_TREE = {"type": "div", "data": {"attrID": "mk-home"},
1190
- "children": [box("mk-doc", {}, [
1325
+ "children": [BOOT, box("mk-doc", {}, [
1191
1326
  MARKS, INDEX, CUE,
1192
1327
  MASTHEAD, TICKER_BAND, SERVICE_SEC, PROCESS_SEC, WORK_SEC,
1193
1328
  STACK_SEC, PRODUCT_SEC, VOICE_SEC, CONTACT,