mosaic-headless 1.29.2 → 1.30.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/README.md +17 -5
- package/SKILL.md +41 -2
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/package.json +9 -2
- package/references/responsive.md +1 -0
- package/scripts/check-release.mjs +5 -1
- package/scripts/sync-version.mjs +3 -2
- package/sites/_components.py +560 -0
- package/sites/_content.py +341 -0
- package/sites/_conversion.py +225 -0
- package/sites/_dialogs.py +579 -0
- package/sites/_dynamic.py +311 -0
- package/sites/_forms.py +487 -0
- package/sites/_sliders.py +437 -0
- package/sites/_tokens.py +277 -0
- package/tools/build_page.py +4 -2
- package/tools/build_site.py +4 -2
- package/tools/copy_styles.py +4 -2
- package/tools/mosaic_config.py +71 -0
- package/tools/probe.py +4 -2
- package/tools/probe_accordion.py +4 -2
- package/tools/sweep_components.py +4 -2
- package/tools/sweep_interactions.py +4 -2
- package/tools/sweep_node_properties.py +4 -2
- package/tools/sweep_node_types.py +4 -2
- package/tools/sweep_properties.py +4 -2
- package/tools/sweep_style_properties.py +4 -2
- package/tools/sweep_style_states.py +4 -2
- package/tools/theme_zip.py +6 -3
- package/tools/verify_browser.py +4 -2
- package/tools/verify_intro.py +4 -2
- package/tools/verify_rwd.py +4 -2
- package/sites/moksa.json +0 -63681
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""The slider lab: four animations, a carousel, and a slider that is not a carousel.
|
|
2
|
+
|
|
3
|
+
The complaint this page was built to answer was aesthetic, not technical — someone
|
|
4
|
+
looked at a Mosaic rebuild and said the slider "looked a bit funny". It usually
|
|
5
|
+
does, and the reason is almost never the component: a slider defaults to being a
|
|
6
|
+
photo carousel, and a photo carousel with stock images and a dot row under it looks
|
|
7
|
+
like 2011 whatever renders it.
|
|
8
|
+
|
|
9
|
+
So nothing here is a photograph. Every slide is type on a colour field, which is
|
|
10
|
+
both the better-looking answer and the more honest demonstration — `slider-slide`
|
|
11
|
+
takes arbitrary children, so a slide is a layout, not a picture frame. The four
|
|
12
|
+
`animation` values then sit side by side at the same size and the same timing,
|
|
13
|
+
which is the only way to actually see what distinguishes them.
|
|
14
|
+
|
|
15
|
+
Two facts from `references/slider.md` drive the structure, and both are things the
|
|
16
|
+
markup does not announce:
|
|
17
|
+
|
|
18
|
+
- a `slider-navigation-bullet` is a TEMPLATE — you author one and Mosaic repeats it
|
|
19
|
+
per slide, giving every copy the same id, so nothing here addresses a bullet by
|
|
20
|
+
attrID;
|
|
21
|
+
- an arrow hides itself at the end it cannot pass unless the slider is a carousel,
|
|
22
|
+
which is why the hero's arrows disappear at the ends and the carousel's never do.
|
|
23
|
+
"""
|
|
24
|
+
import uuid
|
|
25
|
+
|
|
26
|
+
from _moksa import (CJK, DISPLAY, FOCUS_RING, MONO, RULE, T, bp, box, grid, ml,
|
|
27
|
+
mono, section, wrap)
|
|
28
|
+
|
|
29
|
+
U = lambda: str(uuid.uuid4())
|
|
30
|
+
|
|
31
|
+
INK = "rgb(22,24,28)"
|
|
32
|
+
PAPER = "rgb(250,250,247)"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------- pieces
|
|
36
|
+
|
|
37
|
+
def slide(attr, title, bg, fg, index, head, caption, big="64px", big_m="40px",
|
|
38
|
+
pad_bottom="86px", pad_bottom_m="96px", ghost=None, counter=""):
|
|
39
|
+
"""One slide: a colour field composed like a cover, not a captioned photo.
|
|
40
|
+
|
|
41
|
+
`title` is not decoration - Mosaic reads it for the `aria-label` of the
|
|
42
|
+
navigation bullet that stands for this slide, so it is the slide's accessible
|
|
43
|
+
name and has to say something.
|
|
44
|
+
|
|
45
|
+
The first version pushed a label to the top and a headline to the bottom with
|
|
46
|
+
`justify-content: space-between`, which on a 440px field left a hole through
|
|
47
|
+
the middle and read as unfinished. This one hangs everything off a meta rule
|
|
48
|
+
at the top, lets the type block sit where it falls, and puts a big ghost
|
|
49
|
+
numeral in the corner so the field has something in it.
|
|
50
|
+
"""
|
|
51
|
+
kids = []
|
|
52
|
+
if ghost:
|
|
53
|
+
kids.append(ml("p", ghost, fontFamily=DISPLAY, fontWeight="600",
|
|
54
|
+
fontSize="200px", lineHeight="0.78", letterSpacing="-0.02em",
|
|
55
|
+
color=fg, position="absolute",
|
|
56
|
+
customDeclarations="right:-10px;bottom:-34px;opacity:.09;"
|
|
57
|
+
"pointer-events:none;",
|
|
58
|
+
_m={"fontSize": "120px"}))
|
|
59
|
+
kids += [
|
|
60
|
+
# the meta rule: what this slide is, and where you are in the set
|
|
61
|
+
box(attr + "-meta", {
|
|
62
|
+
"display": "flex", "justifyContent": "space-between",
|
|
63
|
+
"alignItems": "baseline", "columnGap": "16px", "width": "100%",
|
|
64
|
+
"paddingBottom": "12px",
|
|
65
|
+
"customDeclarations": "border-bottom:1px solid rgba(250,250,247,.22);"},
|
|
66
|
+
[mono(index, size="11px", color=fg, track="0.3em"),
|
|
67
|
+
mono(counter, size="11px", color=fg, track="0.2em",
|
|
68
|
+
customDeclarations="opacity:.55;")]),
|
|
69
|
+
box(attr + "-body", {
|
|
70
|
+
"marginTop": "28px", "position": "relative", "maxWidth": "640px"},
|
|
71
|
+
[ml("p", head, fontFamily=DISPLAY, fontWeight="600",
|
|
72
|
+
fontSize=big, lineHeight="1.04", letterSpacing="0.005em",
|
|
73
|
+
color=fg, whiteSpace="pre-wrap", _m={"fontSize": big_m}),
|
|
74
|
+
T("p", caption, fontFamily=CJK, fontSize="14px",
|
|
75
|
+
lineHeight="1.85", color=fg, marginTop="16px",
|
|
76
|
+
maxWidth="430px", customDeclarations="opacity:.8;",
|
|
77
|
+
_m={"fontSize": "13px"})] if caption else
|
|
78
|
+
[ml("p", head, fontFamily=DISPLAY, fontWeight="600",
|
|
79
|
+
fontSize=big, lineHeight="1.04", letterSpacing="0.005em",
|
|
80
|
+
color=fg, whiteSpace="pre-wrap", _m={"fontSize": big_m})]),
|
|
81
|
+
]
|
|
82
|
+
return {"type": "slider-slide",
|
|
83
|
+
"data": {"attrID": attr, "title": title},
|
|
84
|
+
# the attrID and this style land on the inner .m-slide-content.
|
|
85
|
+
# It arrives as display:flex with align-items:CENTER, so left-aligned
|
|
86
|
+
# type has to say so or every slide centres itself and looks like the
|
|
87
|
+
# stock carousel this page exists to argue against.
|
|
88
|
+
"style": bp({"backgroundColor": bg, "display": "flex",
|
|
89
|
+
"flexDirection": "column", "justifyContent": "flex-start",
|
|
90
|
+
"alignItems": "flex-start", "height": "100%",
|
|
91
|
+
"position": "relative", "overflow": "hidden",
|
|
92
|
+
"paddingTop": "30px", "paddingBottom": pad_bottom,
|
|
93
|
+
"paddingLeft": "36px", "paddingRight": "36px"},
|
|
94
|
+
None,
|
|
95
|
+
{"paddingTop": "20px", "paddingBottom": pad_bottom_m,
|
|
96
|
+
"paddingLeft": "20px", "paddingRight": "20px"}),
|
|
97
|
+
"children": kids}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def arrow(side, fg):
|
|
101
|
+
"""An arrow ships with NO glyph and accepts no children.
|
|
102
|
+
|
|
103
|
+
`mosaic-slider-arrow-left` renders with empty innerHTML and its placement rule
|
|
104
|
+
is `none`, so the whole appearance is the author's CSS on the element itself.
|
|
105
|
+
The first version drew a two-border chevron, which looked like a stray 9px
|
|
106
|
+
hairline and was far too small to hit; this is a 38px square with the chevron
|
|
107
|
+
as a background image, which is both a control and a target.
|
|
108
|
+
"""
|
|
109
|
+
# Three things this URI must not contain. No quotes, because one that has to
|
|
110
|
+
# survive a Python string, a JSON commit and a stylesheet is one quote too
|
|
111
|
+
# many. No raw spaces. And no SEMICOLON - `customDeclarations` is a list of
|
|
112
|
+
# declarations separated by `;`, so `data:image/svg+xml;charset=utf-8` splits
|
|
113
|
+
# the rule in half and the whole thing is dropped, silently: the arrow came
|
|
114
|
+
# back with background-image:none and no border at all.
|
|
115
|
+
chev = ("url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23FAFAF7'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M9%205l7%207-7%207'/%3E%3C/svg%3E)"
|
|
116
|
+
if side == "right" else "url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23FAFAF7'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M15%205l-7%207%207%207'/%3E%3C/svg%3E)")
|
|
117
|
+
return {"type": "slider-arrow-%s" % side,
|
|
118
|
+
"data": {"attrID": "sl-%s" % side},
|
|
119
|
+
"style": bp({
|
|
120
|
+
"width": "38px", "height": "38px", "cursor": "pointer",
|
|
121
|
+
"backgroundColor": "rgba(250,250,247,0)",
|
|
122
|
+
"customDeclarations":
|
|
123
|
+
"border:1px solid rgba(250,250,247,.34);"
|
|
124
|
+
"background-image:" + chev + ";"
|
|
125
|
+
"background-repeat:no-repeat;background-position:center;"
|
|
126
|
+
"background-size:17px 17px;"
|
|
127
|
+
"transition:background-color .18s ease,border-color .18s ease;"},
|
|
128
|
+
None, {"width": "42px", "height": "42px"}),
|
|
129
|
+
"hover": {"_": {
|
|
130
|
+
"backgroundColor": "rgba(250,250,247,.14)",
|
|
131
|
+
"customDeclarations":
|
|
132
|
+
"border:1px solid rgba(250,250,247,.9);"
|
|
133
|
+
"background-image:" + chev + ";"
|
|
134
|
+
"background-repeat:no-repeat;background-position:center;"
|
|
135
|
+
"background-size:17px 17px;"}},
|
|
136
|
+
"focus-visible": {"_": dict(FOCUS_RING)}}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def bullets(attr, fg):
|
|
140
|
+
"""ONE bullet. Mosaic repeats it per slide; authoring three gives three
|
|
141
|
+
templates, which is the mistake this comment exists to prevent."""
|
|
142
|
+
return {"type": "slider-navigation",
|
|
143
|
+
"data": {"attrID": attr},
|
|
144
|
+
"style": {"&": {"_": {"display": "flex", "columnGap": "10px",
|
|
145
|
+
"alignItems": "center"}}},
|
|
146
|
+
"children": [
|
|
147
|
+
{"type": "slider-navigation-bullet",
|
|
148
|
+
"data": {"attrID": attr + "-dot"},
|
|
149
|
+
"style": {"&": {"_": {
|
|
150
|
+
"width": "30px", "height": "2px", "cursor": "pointer",
|
|
151
|
+
"backgroundColor": "rgba(250,250,247,.34)",
|
|
152
|
+
"customDeclarations":
|
|
153
|
+
"transition:background-color .2s ease;"},
|
|
154
|
+
# the mark stays a hairline; the hit area does not
|
|
155
|
+
"_t": {"width": "34px", "height": "2px",
|
|
156
|
+
"paddingTop": "14px", "paddingBottom": "14px",
|
|
157
|
+
"customDeclarations":
|
|
158
|
+
"background-clip:content-box;"
|
|
159
|
+
"transition:background-color .2s ease;"}},
|
|
160
|
+
"___slider_navigation_bullet--active": {
|
|
161
|
+
"_": {"backgroundColor": "rgb(255,90,54)",
|
|
162
|
+
"customDeclarations": "opacity:1;"}},
|
|
163
|
+
"___slider_navigation_bullet--hover": {
|
|
164
|
+
"_": {"backgroundColor": "rgba(250,250,247,.75)"}},
|
|
165
|
+
"focus-visible": {"_": dict(FOCUS_RING)}}}]}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def slider(attr, slides, animation="slide", autoplay="3600", carousel="0",
|
|
169
|
+
label="", height="440px", height_m="360px", chrome=True,
|
|
170
|
+
fg=PAPER, limit="0", duration="700", start="0"):
|
|
171
|
+
kids = [{"type": "slider-slides",
|
|
172
|
+
"data": {"attrID": attr + "-track"},
|
|
173
|
+
"children": slides}]
|
|
174
|
+
if chrome:
|
|
175
|
+
kids += [
|
|
176
|
+
box(attr + "-chrome", {
|
|
177
|
+
"position": "absolute", "left": "0px", "right": "0px",
|
|
178
|
+
"bottom": "0px",
|
|
179
|
+
"display": "flex", "justifyContent": "space-between",
|
|
180
|
+
"alignItems": "center", "columnGap": "16px",
|
|
181
|
+
"paddingLeft": "36px", "paddingRight": "36px",
|
|
182
|
+
"paddingTop": "16px", "paddingBottom": "22px",
|
|
183
|
+
"customDeclarations":
|
|
184
|
+
"border-top:1px solid rgba(250,250,247,.16);"
|
|
185
|
+
"margin-left:36px;margin-right:36px;"},
|
|
186
|
+
[bullets(attr + "-nav", fg),
|
|
187
|
+
box(attr + "-arrows", {"display": "flex", "columnGap": "14px",
|
|
188
|
+
"alignItems": "center"},
|
|
189
|
+
[arrow("left", fg), arrow("right", fg)])],
|
|
190
|
+
_m={"paddingLeft": "0px", "paddingRight": "0px",
|
|
191
|
+
"paddingBottom": "18px",
|
|
192
|
+
"customDeclarations":
|
|
193
|
+
"border-top:1px solid rgba(250,250,247,.16);"
|
|
194
|
+
"margin-left:20px;margin-right:20px;"}),
|
|
195
|
+
]
|
|
196
|
+
return {"type": "slider",
|
|
197
|
+
"data": {"attrID": attr, "animation": animation,
|
|
198
|
+
"duration": duration, "easing": "ease",
|
|
199
|
+
"isAutoplay": "1" if autoplay else "0",
|
|
200
|
+
"autoplayDelay": autoplay or "3000",
|
|
201
|
+
"autoplayLimit": limit,
|
|
202
|
+
"isCarousel": carousel, "defaultSlideIndex": start,
|
|
203
|
+
"ariaLabel": label},
|
|
204
|
+
"style": bp({"position": "relative", "height": height,
|
|
205
|
+
"overflow": "hidden", "backgroundColor": INK},
|
|
206
|
+
None, {"height": height_m}),
|
|
207
|
+
"children": kids}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def spec_line(label, value):
|
|
211
|
+
return box("sl-spec-row", {
|
|
212
|
+
"display": "flex", "columnGap": "12px", "alignItems": "baseline",
|
|
213
|
+
"paddingTop": "5px", "paddingBottom": "5px",
|
|
214
|
+
"customDeclarations": "border-top:1px solid " + RULE + ";"}, [
|
|
215
|
+
box("sl-spec-k", {"width": "86px", "flexSizing": {"type": "none"}},
|
|
216
|
+
[mono(label, size="10px", color="--mk-faint", track="0.16em")]),
|
|
217
|
+
box("sl-spec-v", {"flexSizing": {"type": "custom", "customOptions": {"flexGrow": "1", "flexShrink": "1"}}, "minWidth": "0px"},
|
|
218
|
+
[mono(value, size="10px", color="--mk-ink", track="0.04em")])])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------- the hero
|
|
222
|
+
|
|
223
|
+
HERO_SLIDES = [
|
|
224
|
+
slide("sl-h-0", "一、輪播不是相簿",
|
|
225
|
+
"rgb(22,24,28)", PAPER, "01 / FORM",
|
|
226
|
+
"輪播\n不是相簿。",
|
|
227
|
+
"slider-slide 接任意子節點,"
|
|
228
|
+
"所以一張投影片是一個版面,"
|
|
229
|
+
"不是一個相框。這一頁沒有任何照片。",
|
|
230
|
+
counter="01 — 04", ghost="1"),
|
|
231
|
+
slide("sl-h-1", "二、四種動畫",
|
|
232
|
+
"rgb(31,58,95)", PAPER, "02 / ANIMATION",
|
|
233
|
+
"四種動畫,\n一個字段。",
|
|
234
|
+
"animation 接 slide、crossFade、fadeOut、fadeOver。"
|
|
235
|
+
"下面四個同尺寸同速度的輪播並排,"
|
|
236
|
+
"是唯一看得出差別的方式。",
|
|
237
|
+
counter="02 — 04", ghost="2"),
|
|
238
|
+
slide("sl-h-2", "三、箭頭會自己消失",
|
|
239
|
+
"rgb(191,68,40)", PAPER, "03 / BEHAVIOUR",
|
|
240
|
+
"箭頭會在\n端點自己消失。",
|
|
241
|
+
"非輪轉模式下,Slider.js 在第一張藏起向前、"
|
|
242
|
+
"最後一張藏起向後。那是一個真正的樣式狀態,"
|
|
243
|
+
"可以自己接手設計。",
|
|
244
|
+
counter="03 — 04", ghost="3"),
|
|
245
|
+
slide("sl-h-3", "四、不想動就不動",
|
|
246
|
+
"rgb(90,92,97)", PAPER, "04 / MOTION",
|
|
247
|
+
"不想動的人,\n它就不動。",
|
|
248
|
+
"系統設成減少動態時,Mosaic 自己停掉自動播放,"
|
|
249
|
+
"作者不用寫任何東西——這是實測過的。",
|
|
250
|
+
counter="04 — 04", ghost="4"),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
HERO = section("sl-hero", [wrap("sl-hero-w", [
|
|
254
|
+
box("sl-hero-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
|
|
255
|
+
mono("SLIDER LAB / 輪播實驗室", size="11px",
|
|
256
|
+
color="--mk-faint", track="0.26em"),
|
|
257
|
+
ml("h1", "輪播看起來很舅,\n通常不是輪播的錯。",
|
|
258
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="54px", lineHeight="1.08",
|
|
259
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
|
|
260
|
+
_t={"fontSize": "42px"}, _m={"fontSize": "30px"}),
|
|
261
|
+
T("p",
|
|
262
|
+
"一個輪播預設會被做成圖片跑馬燈,"
|
|
263
|
+
"而圖片跑馬燈配上庫存照片和一排小圓點,"
|
|
264
|
+
"用什麼工具做都像 2011 年。"
|
|
265
|
+
"這一頁沒有任何照片:每一張投影片都是色塊上的排版。",
|
|
266
|
+
fontFamily=CJK, fontSize="16px", lineHeight="1.95",
|
|
267
|
+
color={"token": "--mk-ink"}, marginTop="20px", maxWidth="660px"),
|
|
268
|
+
box("sl-hero-gap", {"paddingTop": "34px"}, []),
|
|
269
|
+
# carousel="1" is not decoration here: without it autoplay runs to the last
|
|
270
|
+
# slide and STOPS, which on a hero reads as the page having frozen. It is
|
|
271
|
+
# also what keeps both arrows alive, so nothing ever dead-ends.
|
|
272
|
+
slider("sl-main", HERO_SLIDES, animation="slide", autoplay="3600",
|
|
273
|
+
carousel="1",
|
|
274
|
+
label="輪播示範:四張投影片",
|
|
275
|
+
height_m="440px"),
|
|
276
|
+
box("sl-hero-foot", {"paddingTop": "60px"}, []),
|
|
277
|
+
])], pad_y="0px")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------- the four
|
|
281
|
+
|
|
282
|
+
def mini(key, anim, name, blurb, start="0"):
|
|
283
|
+
sl = [slide("sl-%s-%d" % (key, i), "%s %d" % (name, i + 1), c, PAPER,
|
|
284
|
+
"%02d" % (i + 1), t, "", big="30px", big_m="24px",
|
|
285
|
+
pad_bottom="26px", pad_bottom_m="20px", ghost=str(i + 1),
|
|
286
|
+
counter="%d / 3" % (i + 1))
|
|
287
|
+
for i, (c, t) in enumerate([("rgb(22,24,28)", "ONE"),
|
|
288
|
+
("rgb(31,58,95)", "TWO"),
|
|
289
|
+
("rgb(191,68,40)", "THREE")])]
|
|
290
|
+
return box("sl-card-" + key, {
|
|
291
|
+
"display": "flex", "flexDirection": "column", "rowGap": "0px"}, [
|
|
292
|
+
# likewise: three slides and then a freeze is not a comparison
|
|
293
|
+
slider("sl-" + key, sl, animation=anim, autoplay="1800", carousel="1",
|
|
294
|
+
label=name, height="190px", height_m="170px", chrome=False,
|
|
295
|
+
duration="600", start=start),
|
|
296
|
+
box("sl-card-h-" + key, {
|
|
297
|
+
"display": "flex", "justifyContent": "space-between",
|
|
298
|
+
"alignItems": "baseline", "columnGap": "10px", "marginTop": "12px"},
|
|
299
|
+
[mono(anim, size="10px", color="--mk-accent-ink", track="0.18em"),
|
|
300
|
+
mono(name, size="10px", color="--mk-faint", track="0.16em")]),
|
|
301
|
+
T("p", blurb, fontFamily=CJK, fontSize="13px", lineHeight="1.8",
|
|
302
|
+
color={"token": "--mk-muted"}, marginTop="8px"),
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
FOUR = section("sl-four", [wrap("sl-four-w", [
|
|
307
|
+
box("sl-four-pad", {"paddingTop": "60px"}, []),
|
|
308
|
+
mono("ANIMATION", size="10px", color="--mk-faint", track="0.26em"),
|
|
309
|
+
ml("h2", "四種動畫,並排看。",
|
|
310
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
|
|
311
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
|
|
312
|
+
_m={"fontSize": "24px"}),
|
|
313
|
+
T("p",
|
|
314
|
+
"同樣三張投影片、同樣 1800ms、同樣尺寸。"
|
|
315
|
+
"分開看的時候四種動畫差不多,"
|
|
316
|
+
"並排才看得出來。",
|
|
317
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.9",
|
|
318
|
+
color={"token": "--mk-muted"}, marginTop="10px", maxWidth="620px"),
|
|
319
|
+
box("sl-four-gap", {"paddingTop": "26px"}, []),
|
|
320
|
+
grid("sl-four-grid", 4, "20px", [
|
|
321
|
+
mini("a", "slide", "SLIDE",
|
|
322
|
+
"横向推進。最像「下一張」,"
|
|
323
|
+
"也最適合有前後順序的內容。", start="0"),
|
|
324
|
+
mini("b", "crossFade", "CROSSFADE",
|
|
325
|
+
"兩張同時淡入淡出。沒有方向感,"
|
|
326
|
+
"適合互不相屬的張數。", start="1"),
|
|
327
|
+
mini("c", "fadeOut",
|
|
328
|
+
"FADEOUT", "先淡出再換。中間會路出底色,"
|
|
329
|
+
"節奏比 crossFade 慢半拍。", start="2"),
|
|
330
|
+
mini("d", "fadeOver",
|
|
331
|
+
"FADEOVER", "新的那張疊上來。"
|
|
332
|
+
"沒有空窗,收尾比 fadeOut 乾淨。", start="1"),
|
|
333
|
+
], tcols=2, mcols=1),
|
|
334
|
+
box("sl-four-foot", {"paddingTop": "64px"}, []),
|
|
335
|
+
])], bg="--mk-panel", pad_y="0px")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------- carousel
|
|
339
|
+
|
|
340
|
+
CAR_SLIDES = [
|
|
341
|
+
slide("sl-c-%d" % i, t, c, PAPER, "%02d" % (i + 1), t, "",
|
|
342
|
+
big="22px", big_m="20px", pad_bottom="60px", pad_bottom_m="60px",
|
|
343
|
+
counter="%d / 6" % (i + 1))
|
|
344
|
+
for i, (c, t) in enumerate([
|
|
345
|
+
("rgb(22,24,28)", "WEB"), ("rgb(31,58,95)", "AI"),
|
|
346
|
+
("rgb(191,68,40)", "ERP"), ("rgb(90,92,97)", "SEO"),
|
|
347
|
+
("rgb(22,24,28)", "SHOP"), ("rgb(31,58,95)", "AUTOMATION")])]
|
|
348
|
+
|
|
349
|
+
CAROUSEL = section("sl-car", [wrap("sl-car-w", [
|
|
350
|
+
box("sl-car-pad", {"paddingTop": "60px"}, []),
|
|
351
|
+
mono("CAROUSEL", size="10px", color="--mk-faint", track="0.26em"),
|
|
352
|
+
ml("h2", "輪轉模式:一次看得到好幾張。",
|
|
353
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
|
|
354
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
|
|
355
|
+
_m={"fontSize": "24px"}),
|
|
356
|
+
T("p",
|
|
357
|
+
"isCarousel 做的是一件事:讓頭尾相接。"
|
|
358
|
+
"實測過,它不會讓你一次看到多張——"
|
|
359
|
+
"軌道是一個只有一個 slide 區域的 grid,"
|
|
360
|
+
"不管是不是輪轉,每一張都占滿整個軌道寬。"
|
|
361
|
+
"差別在於輪轉會在第一張前面先擺好一張(left 是負的),"
|
|
362
|
+
"所以沒有「過不去的端點」,箭頭也就不會自己藏起來。",
|
|
363
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.9",
|
|
364
|
+
color={"token": "--mk-muted"}, marginTop="10px", maxWidth="620px"),
|
|
365
|
+
box("sl-car-gap", {"paddingTop": "26px"}, []),
|
|
366
|
+
slider("sl-carousel", CAR_SLIDES, animation="slide", autoplay="2400",
|
|
367
|
+
carousel="1", label="輪轉示範", height="180px",
|
|
368
|
+
height_m="150px", chrome=True, duration="600"),
|
|
369
|
+
box("sl-car-foot", {"paddingTop": "60px"}, []),
|
|
370
|
+
])], pad_y="0px")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------- the notes
|
|
374
|
+
|
|
375
|
+
NOTES = section("sl-notes", [wrap("sl-notes-w", [
|
|
376
|
+
box("sl-notes-inner", {
|
|
377
|
+
"paddingTop": "54px", "paddingBottom": "84px",
|
|
378
|
+
"customDeclarations": "border-top:1px solid " + RULE + ";"}, [
|
|
379
|
+
mono("WHAT THE MARKUP DOES NOT TELL YOU", size="10px",
|
|
380
|
+
color="--mk-faint", track="0.26em"),
|
|
381
|
+
box("sl-notes-grid-w", {"marginTop": "22px"}, [
|
|
382
|
+
grid("sl-notes-grid", 3, "22px", [
|
|
383
|
+
box("sl-note-1", {}, [
|
|
384
|
+
ml("h3", "小圓點是一個模板",
|
|
385
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="17px",
|
|
386
|
+
lineHeight="1.3", color={"token": "--mk-ink"}),
|
|
387
|
+
T("p",
|
|
388
|
+
"你只寫一個 slider-navigation-bullet,"
|
|
389
|
+
"Mosaic 會按投影片數量複製它。"
|
|
390
|
+
"所有複製品共用同一個 id,"
|
|
391
|
+
"差別只在 aria-label——而那個標籤"
|
|
392
|
+
"是從每張投影片的 title 來的。"
|
|
393
|
+
"所以別用 attrID 找小圓點,要用位置。",
|
|
394
|
+
fontFamily=CJK, fontSize="13px", lineHeight="1.85",
|
|
395
|
+
color={"token": "--mk-muted"}, marginTop="10px"),
|
|
396
|
+
box("sl-note-1-s", {"marginTop": "14px"}, [
|
|
397
|
+
spec_line("寫了", "1 個 bullet"),
|
|
398
|
+
spec_line("得到", "4 個(每張一個)")]),
|
|
399
|
+
]),
|
|
400
|
+
box("sl-note-2", {}, [
|
|
401
|
+
ml("h3", "箭頭會自己藏起來",
|
|
402
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="17px",
|
|
403
|
+
lineHeight="1.3", color={"token": "--mk-ink"}),
|
|
404
|
+
T("p",
|
|
405
|
+
"在第一張時向前箭頭會拿到 "
|
|
406
|
+
"m-slider-arrow--hidden,最後一張時輪到向後箭頭,"
|
|
407
|
+
"輪轉模式則兩者都不會。"
|
|
408
|
+
"測試時要記得:自動播放停在最後一張之後,"
|
|
409
|
+
"向後箭頭是「合法地消失」,不是壞了。",
|
|
410
|
+
fontFamily=CJK, fontSize="13px", lineHeight="1.85",
|
|
411
|
+
color={"token": "--mk-muted"}, marginTop="10px"),
|
|
412
|
+
box("sl-note-2-s", {"marginTop": "14px"}, [
|
|
413
|
+
spec_line("狀態", "___arrow_hidden"),
|
|
414
|
+
spec_line("可設計", "是,是真正的樣式狀態")]),
|
|
415
|
+
]),
|
|
416
|
+
box("sl-note-3", {}, [
|
|
417
|
+
ml("h3", "減少動態是它自己處理的",
|
|
418
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="17px",
|
|
419
|
+
lineHeight="1.3", color={"token": "--mk-ink"}),
|
|
420
|
+
T("p",
|
|
421
|
+
"作業系統設成減少動態時,"
|
|
422
|
+
"Mosaic 停掉自動播放,不需要作者寫任何"
|
|
423
|
+
"media query。軌道本身帶 aria-live:"
|
|
424
|
+
"自動播放時是 off,停下來時變成 polite。",
|
|
425
|
+
fontFamily=CJK, fontSize="13px", lineHeight="1.85",
|
|
426
|
+
color={"token": "--mk-muted"}, marginTop="10px"),
|
|
427
|
+
box("sl-note-3-s", {"marginTop": "14px"}, [
|
|
428
|
+
spec_line("自動播放時", "aria-live=off"),
|
|
429
|
+
spec_line("停下時", "aria-live=polite")]),
|
|
430
|
+
]),
|
|
431
|
+
], tcols=2, mcols=1),
|
|
432
|
+
]),
|
|
433
|
+
]),
|
|
434
|
+
])], pad_y="0px")
|
|
435
|
+
|
|
436
|
+
TREE = {"type": "div", "data": {"attrID": "sl-page"},
|
|
437
|
+
"children": [HERO, FOUR, CAROUSEL, NOTES]}
|