mosaic-headless 1.7.0 → 1.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.
package/sites/_moksa.py CHANGED
@@ -334,6 +334,15 @@ BOOT_LOG = [
334
334
  ]
335
335
  RULER_TICKS = 13
336
336
 
337
+ # FIGURE 01's rows. Up here with the other tables rather than beside the section
338
+ # that draws it, because motion_css() reads it to build the arrow stagger and is
339
+ # defined long before the figure is.
340
+ FLOW = [
341
+ ("網站表單", "FORM", "n8n 節點", "WORKFLOW", "ERP 建單", "ERP"),
342
+ ("電商訂單", "ORDER", "Claude 判讀", "REASONING", "通知/報表", "NOTIFY"),
343
+ ("客服訊息", "INBOX", "分類與派工", "TRIAGE", "CRM 紀錄", "CRM"),
344
+ ]
345
+
337
346
  CLAUSES = [
338
347
  ("01", "SERVICES", "#services", "--mk-s1"),
339
348
  ("02", "PROCESS", "#process", "--mk-s2"),
@@ -396,6 +405,13 @@ def motion_css():
396
405
  for i, (_g, items) in enumerate(STACK)
397
406
  for j in range(len(items)))
398
407
  marks = ",".join("#mk-mark-%d" % i for i in range(4))
408
+ idx_words = ",".join("#mk-idx-w-%d" % i for i in range(len(CLAUSES)))
409
+ # the diagram's arrows, and the delay that makes the pulse travel down it
410
+ flow_arrows = ",".join("#mk-ar-%s-%d>*" % (c, i)
411
+ for i in range(len(FLOW)) for c in ("a", "b"))
412
+ flow_stagger = "\n".join(
413
+ " #mk-ar-%s-%d>*{animation-delay:%dms}" % (c, i, i * 260 + j * 130)
414
+ for i in range(len(FLOW)) for j, c in enumerate(("a", "b")))
399
415
  # the boot sequence, phase by phase: the clauses report in, the log writes
400
416
  # itself out a line at a time, and the ruler ticks up the left margin
401
417
  boot_stagger = "\n".join(
@@ -461,6 +477,17 @@ def motion_css():
461
477
  "@keyframes mk-idxon{from,to{color:rgb(255,90,54);"
462
478
  "letter-spacing:.2em}}",
463
479
  "@keyframes mk-dotin{from{transform:scale(0)}to{transform:scale(1)}}",
480
+ # the accent band pans sideways while it crosses the viewport
481
+ "@keyframes mk-pan{from{transform:translateX(2%)}"
482
+ "to{transform:translateX(-34%)}}",
483
+ "@keyframes mk-panloop{from{transform:translateX(0)}"
484
+ "to{transform:translateX(-50%)}}",
485
+ # a headline that fills with ink as it is read past
486
+ "@keyframes mk-fillscrub{from{background-position:100% 0}"
487
+ "to{background-position:0 0}}",
488
+ # a pulse travelling down the diagram's arrows
489
+ "@keyframes mk-flow{0%{transform:translateX(-7px);opacity:.2}"
490
+ "45%{opacity:1}100%{transform:translateX(7px);opacity:.2}}",
464
491
 
465
492
  # ── the document frame ───────────────────────────────────────────────
466
493
  # A spec sheet has margins. These two fixed rules are what say the content
@@ -500,6 +527,44 @@ def motion_css():
500
527
  # belongs to a page set in a terminal face
501
528
  "#mk-caret{width:13px;height:19px;background:rgb(255,90,54)}",
502
529
  "#mk-plate-dash{width:34px;height:1px;background:rgb(191,68,40)}",
530
+
531
+ # ── the clock ────────────────────────────────────────────────────────
532
+ # Two registered integers, stepped rather than eased, so each one lands on a
533
+ # whole number and reads as a tick instead of a blur. `decimal-leading-zero`
534
+ # is what keeps it two digits without any padding logic.
535
+ "@property --mk-s{syntax:'<integer>';initial-value:0;inherits:true}",
536
+ "@property --mk-m{syntax:'<integer>';initial-value:0;inherits:true}",
537
+ "@keyframes mk-secs{to{--mk-s:60}}",
538
+ "@keyframes mk-mins{to{--mk-m:60}}",
539
+ "@keyframes mk-livedot{0%,55%{opacity:1}56%,100%{opacity:.15}}",
540
+ "#mk-clock{display:flex;align-items:baseline;column-gap:5px;"
541
+ "margin-left:18px;padding-left:18px;"
542
+ "border-left:1px solid " + RULE + "}",
543
+ "#mk-clock-dot{width:5px;height:5px;background:rgb(255,90,54);"
544
+ "align-self:center;margin-right:3px}",
545
+ "#mk-clock-m,#mk-clock-s{font-family:" + MONO + ";font-size:11px;"
546
+ "color:rgb(22,24,28);font-variant-numeric:tabular-nums;"
547
+ "letter-spacing:0.02em}",
548
+ "#mk-clock-s::after{counter-reset:s var(--mk-s);"
549
+ "content:counter(s,decimal-leading-zero)}",
550
+ "#mk-clock-m::after{counter-reset:m var(--mk-m);"
551
+ "content:counter(m,decimal-leading-zero)}",
552
+ "@media (max-width:1023px){#mk-clock{display:none}}",
553
+
554
+ # ── an ambient sweep ─────────────────────────────────────────────────
555
+ # One hairline crossing the viewport, forever, at an opacity that is almost
556
+ # an excuse. It is the difference between a page that is still and a page
557
+ # that is idling.
558
+ "@keyframes mk-sweepdown{from{transform:translateY(-10vh)}"
559
+ "to{transform:translateY(110vh)}}",
560
+ '#mk-sweep{content:"";position:fixed;left:0;right:0;top:0;height:1px;'
561
+ "z-index:2;pointer-events:none;background:linear-gradient(90deg,"
562
+ "rgba(255,90,54,0),rgba(255,90,54,.30) 22%,rgba(255,90,54,.30) 78%,"
563
+ "rgba(255,90,54,0))}",
564
+
565
+ # ── the margin rules carry a travelling segment ──────────────────────
566
+ "@keyframes mk-railrun{from{background-position:0 -40vh}"
567
+ "to{background-position:0 140vh}}",
503
568
  "#mk-mast-rule,#mk-spec-rule{height:1px;background:" + RULE + ";"
504
569
  "transform:scaleX(0);transform-origin:0 50%}",
505
570
  digits + "{display:block;transform:translateY(105%)}",
@@ -528,8 +593,23 @@ def motion_css():
528
593
 
529
594
  # the clause index. It needs room outside the document margin, so it only
530
595
  # appears once the viewport is wide enough to have that room.
531
- "#mk-index{position:fixed;left:14px;top:50%;transform:translateY(-50%);"
532
- "z-index:3;display:grid;row-gap:11px;pointer-events:auto}",
596
+ # The index is FIXED, so it does not sit on the ground it was declared
597
+ # against - it sits on whatever has scrolled underneath. Over the accent
598
+ # band its faint grey became unreadable. Giving it its own paper ground
599
+ # means it carries its background with it instead of borrowing one.
600
+ # The index lives in the margin, so where it can show its words is a
601
+ # function of how much margin there is: the container is 1240px centred, so
602
+ # the left margin is (W-1240)/2, and the full panel measures 178px. First
603
+ # attempt put the threshold at 1560 and 1600px still overlapped by 6px -
604
+ # measured, not estimated. 1680 leaves 34px of air; below it the panel keeps
605
+ # the clause numbers only, which fit the gutter at any width.
606
+ "#mk-index{position:fixed;left:8px;top:50%;transform:translateY(-50%);"
607
+ "z-index:3;display:grid;row-gap:11px;pointer-events:auto;"
608
+ "background:rgb(250,250,247);padding:14px 10px;"
609
+ "border:1px solid " + RULE + "}",
610
+ "#mk-index>*{display:flex;column-gap:8px;align-items:baseline}",
611
+ "@media (max-width:1679px){" + idx_words + "{display:none}"
612
+ "#mk-index{padding:12px 7px;left:4px}}",
533
613
  "@media (max-width:1439px){#mk-index{display:none}}",
534
614
 
535
615
  # process: the sequence marker on each step, and the line it sits on
@@ -698,6 +778,19 @@ def motion_css():
698
778
  " #mk-mast-meta{animation-delay:3.06s}",
699
779
  " #mk-mast-lede{animation-delay:3.54s}",
700
780
  " #mk-mast-cta{animation-delay:3.66s}",
781
+ " " + flow_arrows + "{animation:mk-flow 1.9s ease-in-out infinite}",
782
+ " #mk-clock-s{animation:mk-secs 60s steps(60,end) infinite}",
783
+ " #mk-clock-m{animation:mk-mins 3600s steps(60,end) infinite}",
784
+ " #mk-clock-dot{animation:mk-livedot 2s steps(1,end) infinite}",
785
+ " #mk-sweep{animation:mk-sweepdown 11s linear infinite}",
786
+ " #mk-pan-loop{animation:mk-panloop 26s linear infinite}",
787
+ # the two fixed document rules get a lit segment sliding down them forever
788
+ " #mk-doc::before,#mk-doc::after{"
789
+ "background-image:linear-gradient(180deg,rgba(255,90,54,0),"
790
+ "rgba(255,90,54,.55) 45%,rgba(255,90,54,.55) 55%,rgba(255,90,54,0));"
791
+ "background-size:1px 34vh;background-repeat:no-repeat;"
792
+ "animation:mk-railrun 9s linear infinite}",
793
+ flow_stagger,
701
794
  " #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;"
702
795
  "animation-delay:3.84s}",
703
796
  " #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
@@ -731,6 +824,28 @@ def motion_css():
731
824
  # the sequence markers pop as each step arrives
732
825
  " " + proc_dots + "{animation:mk-dotin .01s linear both;"
733
826
  "animation-timeline:view();animation-range:entry 14% cover 26%}",
827
+ # the band pans across the whole time it is on screen
828
+ " #mk-pan-track{animation:mk-pan linear both;"
829
+ "animation-timeline:view();animation-range:cover 0% cover 100%}",
830
+ # The statement fills with ink as it passes. The gradient is built so the
831
+ # UNANIMATED state shows the faint half - `color:transparent` plus
832
+ # `background-clip:text` is one of the few ways to make text that is
833
+ # genuinely invisible if its animation never runs, and the page must never
834
+ # depend on that. At `from` the visible half is the faint colour, so the
835
+ # worst case is a paler headline, not a missing one.
836
+ " #mk-plate-t h2{background-image:linear-gradient(90deg,"
837
+ # The unfilled half is `--mk-faint`, not a decorative pale grey. At
838
+ # rgb(168,170,176) the tail of the line measured 2.3:1 against the panel
839
+ # while the scrub had not reached it - readable only once you had scrolled
840
+ # far enough, which is not a thing to ask of a sentence. This value clears
841
+ # 4.66:1 on its own, so the effect is a change in weight rather than a
842
+ # change between legible and not.
843
+ "rgb(22,24,28) 0 50%,rgb(107,109,113) 50% 100%);"
844
+ "background-size:220% 100%;background-position:100% 0;"
845
+ "-webkit-background-clip:text;background-clip:text;"
846
+ "color:rgba(0,0,0,0);"
847
+ "animation:mk-fillscrub linear both;animation-timeline:view();"
848
+ "animation-range:entry 24% cover 58%}",
734
849
  " }",
735
850
  " @supports (timeline-scope:--x){",
736
851
  # the index entry lights while its own section is on screen. No fill mode, so
@@ -789,6 +904,20 @@ HEADER = box("mk-shell-top", {}, [
789
904
  T("p", "/ STUDIO", color={"token": "--mk-faint"},
790
905
  fontSize="11px", letterSpacing="0.06em", fontFamily=MONO,
791
906
  _m={"display": "none"})]},
907
+ # A running session clock. It counts from the moment the document
908
+ # loaded, which is why it says T+ rather than pretending to be the
909
+ # time of day - a page cannot know that without JavaScript, and
910
+ # labelling a page-load counter "14:32" would be a small lie told
911
+ # in the most trustworthy typeface on the site.
912
+ box("mk-clock", {}, [
913
+ box("mk-clock-dot", {}, []),
914
+ T("p", "T+", color={"token": "--mk-faint"}, fontSize="10px",
915
+ fontFamily=MONO, letterSpacing="0.18em"),
916
+ box("mk-clock-m", {}, []),
917
+ T("p", ":", color={"token": "--mk-faint"}, fontSize="11px",
918
+ fontFamily=MONO),
919
+ box("mk-clock-s", {}, []),
920
+ ]),
792
921
  {"type": "menu", "data": {"attrID": "mk-nav"},
793
922
  "style": bp({"display": "flex", "columnGap": "30px",
794
923
  "alignItems": "center"},
@@ -1343,6 +1472,49 @@ CONTACT = section("contact", [wrap("mk-contact-in", [
1343
1472
  ]),
1344
1473
  ])])
1345
1474
 
1475
+ # ── the pan strip ─────────────────────────────────────────────────────────────
1476
+ # The page has no field of colour anywhere - the accent has only ever been a rule, a
1477
+ # mark or a small block. A band of it, with the whole line panning sideways as you
1478
+ # scroll down, is the one moment here that is purely about being looked at. Ink on
1479
+ # the accent measures 5.73:1, which is why the type is dark rather than light.
1480
+ PAN_WORDS = ["WEB", "COMMERCE", "AUTOMATION", "AI", "ERP", "SEO", "SOFTWARE"]
1481
+
1482
+ PAN = box("mk-pan",
1483
+ {"backgroundColor": {"token": "--mk-accent"},
1484
+ "paddingTop": "34px", "paddingBottom": "34px",
1485
+ "customStyles": "overflow:hidden;"},
1486
+ _m={"paddingTop": "22px", "paddingBottom": "22px"},
1487
+ children=[
1488
+ # Two nested motions, because one transform cannot do both jobs.
1489
+ # The outer track is scrubbed by the scroll; the inner loop runs on
1490
+ # its own clock so the band is still moving when nobody is scrolling.
1491
+ # The word list is emitted TWICE and the loop travels exactly half its
1492
+ # own width, which is what makes the wrap invisible.
1493
+ box("mk-pan-track", {"customStyles": "width:max-content;"}, [
1494
+ box("mk-pan-loop",
1495
+ {"display": "flex", "columnGap": "56px",
1496
+ "alignItems": "baseline",
1497
+ "customStyles": "white-space:nowrap;width:max-content;"},
1498
+ _m={"columnGap": "30px"},
1499
+ children=[
1500
+ node for c in range(2)
1501
+ for i, w in enumerate(PAN_WORDS) for node in (
1502
+ box("mk-pan-n-%d-%d" % (c, i), {},
1503
+ [mono("%02d" % (i + 1), size="11px",
1504
+ color="rgb(22,24,28)", track="0.18em")]),
1505
+ box("mk-pan-w-%d-%d" % (c, i), {},
1506
+ [T("p", w, color={"token": "--mk-ink"},
1507
+ fontSize="58px", fontWeight="600",
1508
+ fontFamily=DISPLAY, letterSpacing="-0.02em",
1509
+ lineHeight="1",
1510
+ _t={"fontSize": "44px"},
1511
+ _m={"fontSize": "31px"})]),
1512
+ )
1513
+ ]),
1514
+ ]),
1515
+ ])
1516
+
1517
+
1346
1518
  # ── the capability matrix ─────────────────────────────────────────────────────
1347
1519
  # The page could tell you what the studio does; a specification SHOWS you, in a form
1348
1520
  # you can read across. Filled, half and hollow marks carry the whole answer without
@@ -1426,11 +1598,6 @@ MATRIX_SEC = section("matrix", [wrap("mk-mx-in", [
1426
1598
  # austere for eleven screens. A diagram is the one element that says something no
1427
1599
  # sentence here can, and drawn in hairline boxes it belongs to the same document
1428
1600
  # rather than arriving from a different one.
1429
- FLOW = [
1430
- ("網站表單", "FORM", "n8n 節點", "WORKFLOW", "ERP 建單", "ERP"),
1431
- ("電商訂單", "ORDER", "Claude 判讀", "REASONING", "通知/報表", "NOTIFY"),
1432
- ("客服訊息", "INBOX", "分類與派工", "TRIAGE", "CRM 紀錄", "CRM"),
1433
- ]
1434
1601
 
1435
1602
 
1436
1603
  def flow_cell(attr, zh, en):
@@ -1646,7 +1813,10 @@ INDEX = box("mk-index", {}, [
1646
1813
  "transitionAll": "220ms ease"}},
1647
1814
  "hover": {"_": {"color": {"token": "--mk-ink"}}},
1648
1815
  "focus-visible": {"_": FOCUS_RING}},
1649
- "text": "§" + num + " " + name}
1816
+ "children": [
1817
+ box("mk-idx-n-%d" % i, {}, [T("p", "§" + num)]),
1818
+ box("mk-idx-w-%d" % i, {}, [T("p", name)]),
1819
+ ]}
1650
1820
  for i, (num, name, href, _tl) in enumerate(CLAUSES)
1651
1821
  ])
1652
1822
 
@@ -1657,11 +1827,11 @@ CUE = box("mk-cue", {}, [
1657
1827
 
1658
1828
  HOME_TREE = {"type": "div", "data": {"attrID": "mk-home"},
1659
1829
  "children": [BOOT, box("mk-doc", {}, [
1660
- MARKS, INDEX, CUE,
1830
+ MARKS, INDEX, CUE, box("mk-sweep", {}, []),
1661
1831
  # paper, panel, paper, ink, paper, ink, paper - the page
1662
1832
  # changes ground five times so it reads as chapters
1663
1833
  MASTHEAD, TICKER_BAND, PLATE, SERVICE_SEC, MATRIX_SEC,
1664
- PROCESS_SEC, FIGURE_SEC, WORK_SEC, SPECIMEN, STACK_SEC,
1834
+ PROCESS_SEC, FIGURE_SEC, WORK_SEC, PAN, SPECIMEN, STACK_SEC,
1665
1835
  PRODUCT_SEC, VOICE_SEC, CONTACT,
1666
1836
  ])]}
1667
1837