@mdbase-dev/ui 0.1.0-beta.121 → 0.1.0-beta.124

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 CHANGED
@@ -22,7 +22,72 @@ applyThemePreference(loadThemePreference());
22
22
  as `--ink` belong to each app.
23
23
  - `theme` stores the System, Light or Dark choice as `mdbase:theme`.
24
24
  - `brand` draws the Frontmatter mark, each app's inverted mark, and the
25
- `mdbase <app>` wordmark. Add `motion.css` to animate the mark.
25
+ `mdbase <app>` wordmark. Add `motion.css` to animate the mark: loops
26
+ (`orbit`, `scan`, `bounce`, `stream`, `sort`, `hop`), entrances
27
+ (`keys-first`, `drop`, `assemble`), one-shot `saved`/`error` signals, and a
28
+ `progress` fill.
29
+ - `mark-activity` is the page-wide store behind the app switcher's mark.
30
+ `SaveNotice` already reports saves and problems to it. For other work, call
31
+ `signalMdbaseMark("saved" | "error")` when a write the person made finishes,
32
+ `trackMdbaseMarkProgress()` (or `useMdbaseMarkProgress(fraction)`) when the
33
+ total is known, and `holdMdbaseMarkBusy(loop)` (or `useMdbaseMarkBusy`) while
34
+ it is not.
26
35
  - `apps` lists the mdbase apps and builds links that open the current
27
36
  collection in another app. Lab and local builds pass their own URLs to
28
37
  `withAppUrls`.
38
+
39
+ ## Shared feedback
40
+
41
+ Import `@mdbase-dev/ui/feedback` and `@mdbase-dev/ui/feedback.css`, alongside
42
+ `tokens.css` and `controls.css`. Place one `FeedbackProvider` above the app shell
43
+ (and its error boundary); place `FeedbackButton` in a quiet, accessible shell
44
+ location. `useFeedback().open("problem")` opens the same form from an error action.
45
+
46
+ ```tsx
47
+ import { FeedbackButton, FeedbackProvider, feedbackApplication,
48
+ resolveFeedbackEndpoint, useFeedback } from "@mdbase-dev/ui/feedback";
49
+ import "@mdbase-dev/ui/feedback.css";
50
+
51
+ <FeedbackProvider
52
+ endpoint={resolveFeedbackEndpoint(configuredFeedbackUrl, development)}
53
+ application={feedbackApplication("mdbase reader", "library", revision, environment)}
54
+ turnstileSiteKey={configuredPublicSiteKey}
55
+ collectionName={optionalCollectionDisplayName}
56
+ >
57
+ <YourAppShell><FeedbackButton /></YourAppShell>
58
+ </FeedbackProvider>;
59
+ ```
60
+
61
+ Products: `mdbase editor`, `mdbase reader`, `mdbase writer`, `mdbase connect`.
62
+ Views must be fixed identifiers, never URLs or document paths. Application/build
63
+ metadata is always included and previewable; reply email, collection name,
64
+ problem diagnostics, and a screenshot are optional. A single, initially collapsed
65
+ **What gets sent** disclosure holds app details and optional diagnostic/collection
66
+ controls, rather than repeating explanations around the form. Collection-name consent
67
+ resets if the name changes. A missing or invalid endpoint hides entry points.
68
+
69
+ For a **user-visible** failure, call `useFeedback().reportError({ code:
70
+ "save_failed" })`. Use the exported `FeedbackErrorCode` union and optional HTTP
71
+ status, not raw errors. Cancellation does nothing; background retries should not
72
+ call it. The bug wiggles twice, at most once per 30 seconds, and never moves with
73
+ reduced motion. It does not install global exception listeners or telemetry.
74
+
75
+ The form opens focused on its description. Capture starts only on **Attach
76
+ screenshot**, closes the native modal before requesting `getDisplayMedia`, then
77
+ restores the form/draft and stops every track. Unsupported browsers can choose an
78
+ image instead. Draw/highlight/blackout, undo/reset, and keyboard markup export one
79
+ flattened raster; blackouts replace whole pixels and are painted last. Uploads
80
+ are bounded PNG/JPEG, dimension-checked before decoding and re-encoded to discard
81
+ metadata and filenames. Only the explicitly included final image is submitted.
82
+ Opening markup excludes the image until Apply; cancelling or failing to apply
83
+ redactions leaves the original excluded unless the user explicitly includes it.
84
+
85
+ Drafts and the 30-event/five-minute diagnostic buffer are in-memory per provider.
86
+ Cancel preserves the draft; successful dismissal clears it. No collection/auth
87
+ SDK, persistence, public issue tracker, or feedback transport other than the
88
+ configured stateless Worker is required. Turnstile loads when the form opens if a
89
+ public key is configured. Submission omits credentials and referrers, retains the
90
+ request ID for unchanged retries, and resets verification tokens after attempts.
91
+
92
+ See [shared-feedback.md](../../docs/shared-feedback.md) for the Worker contract,
93
+ rollout order, integration status, and regression checks.
package/css/brand.css CHANGED
@@ -1,47 +1,50 @@
1
- /* The platform mark. `motion.css` animates the same structure when a motion is set. */
1
+ /* The Frontmatter mark. `motion.css` animates the same structure when a motion is set. */
2
2
  .mdbase-mark {
3
+ --mark-ink: currentColor;
4
+ --mark-accent: var(--color-accent);
3
5
  overflow: visible;
4
6
  }
5
7
 
8
+ /* An app mark is inverted: the bars take the app's colour and the line stays ink. */
9
+ .mdbase-app-mark {
10
+ --mark-ink: var(--app);
11
+ --mark-accent: currentColor;
12
+ color: var(--color-text);
13
+ }
14
+
15
+ .mdbase-app-mark.is-editor { --app: var(--app-editor); }
16
+ .mdbase-app-mark.is-reader { --app: var(--app-reader); }
17
+ .mdbase-app-mark.is-writer { --app: var(--app-writer); }
18
+
6
19
  .mdbase-mark .mdbase-mark-segment,
7
20
  .mdbase-mark .mdbase-mark-row,
8
- .mdbase-mark .mdbase-mark-fence,
9
- .mdbase-mark .mdbase-mark-conveyor-track {
21
+ .mdbase-mark .mdbase-mark-fence {
10
22
  transform-box: fill-box;
11
23
  transform-origin: center;
12
24
  }
13
25
 
14
26
  .mdbase-mark .mdbase-mark-ink,
15
- .mdbase-mark .mdbase-mark-conveyor-track {
16
- fill: currentColor;
27
+ .mdbase-mark .mdbase-mark-track {
28
+ fill: var(--mark-ink);
17
29
  }
18
30
 
19
31
  .mdbase-mark .mdbase-mark-accent {
20
- fill: var(--color-accent);
21
- }
22
-
23
- .mdbase-mark .mdbase-mark-conveyor-track {
24
- opacity: 0;
25
- transform-box: view-box;
26
- transform-origin: 0 0;
32
+ fill: var(--mark-accent);
27
33
  }
28
34
 
29
- /* An app mark takes its app's colour for the bars; the line stays ink. */
30
- .mdbase-app-mark {
31
- color: var(--color-text);
32
- overflow: visible;
35
+ /* Progress: a faint copy keeps the shape visible while the lines fill. */
36
+ .mdbase-mark .mdbase-mark-progress-ghost {
37
+ opacity: 0.22;
33
38
  }
34
39
 
35
- .mdbase-app-mark.is-editor { --app: var(--app-editor); }
36
- .mdbase-app-mark.is-reader { --app: var(--app-reader); }
37
- .mdbase-app-mark.is-writer { --app: var(--app-writer); }
38
-
39
- .mdbase-app-mark-bars {
40
- fill: var(--app);
40
+ .mdbase-mark .mdbase-mark-progress-fill {
41
+ transition: width 240ms cubic-bezier(0.22, 1, 0.36, 1);
41
42
  }
42
43
 
43
- .mdbase-app-mark-line {
44
- fill: currentColor;
44
+ @media (prefers-reduced-motion: reduce) {
45
+ .mdbase-mark .mdbase-mark-progress-fill {
46
+ transition: none;
47
+ }
45
48
  }
46
49
 
47
50
  .wordmark {
@@ -0,0 +1,62 @@
1
+ .mdbase-feedback-app { display: contents; }
2
+ .mdbase-feedback-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
3
+ .mdbase-feedback-trigger { display: inline-flex; align-items: center; gap: 8px; padding: 8px 10px; border: 0; border-radius: 4px; background: transparent; color: var(--color-text-soft); font: 12px/1.4 var(--sans); cursor: pointer; }
4
+ .mdbase-feedback-trigger:hover { background: var(--color-selected); color: var(--color-text); }
5
+ .mdbase-feedback-bug { width: 22px; height: 22px; flex-shrink: 0; }
6
+ @keyframes mdbase-feedback-wiggle { 0%,100% { transform: rotate(0); } 25% { transform: rotate(-14deg); } 75% { transform: rotate(14deg); } }
7
+ @media (prefers-reduced-motion: no-preference) { .mdbase-feedback-bug.is-wiggling { animation: mdbase-feedback-wiggle 280ms ease-in-out 2; } }
8
+ .mdbase-feedback-dialog { position: fixed; inset: 0 0 0 auto; margin: 0; padding: 0; width: 440px; max-width: 100vw; height: 100dvh; max-height: 100dvh; border: 0; border-left: 1px solid var(--color-border-strong); background: var(--color-canvas); color: var(--color-text); font: 13px/1.5 var(--sans); overflow: hidden; }
9
+ .mdbase-feedback-dialog[open] { display: flex; flex-direction: column; }
10
+ .mdbase-feedback-dialog:not([open]) { display: none; }
11
+ .mdbase-feedback-dialog::backdrop { background: var(--color-scrim); }
12
+ .mdbase-feedback-dialog *, .mdbase-feedback-markup * { box-sizing: border-box; }
13
+ .mdbase-feedback-dialog :is(button,input,textarea), .mdbase-feedback-markup button { font: inherit; }
14
+ .mdbase-feedback-dialog :focus-visible, .mdbase-feedback-markup :focus-visible, .mdbase-feedback-trigger:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
15
+ .mdbase-feedback-dialog button, .mdbase-feedback-markup .mdbase-feedback-actions button, .mdbase-feedback-tools button { border: 1px solid var(--color-border-strong); border-radius: 4px; padding: 7px 11px; background: var(--color-canvas); color: var(--color-text); cursor: pointer; }
16
+ .mdbase-feedback-dialog button:hover, .mdbase-feedback-markup button:hover { background: var(--color-selected); }
17
+ .mdbase-feedback-dialog button:disabled, .mdbase-feedback-markup button:disabled { opacity: .5; cursor: default; }
18
+ .mdbase-feedback-header { display: flex; justify-content: space-between; align-items: center; gap: 15px; padding: 27px; border-bottom: 1px solid var(--color-border); flex-shrink: 0; }
19
+ .mdbase-feedback-header h2 { font-size: 23px; line-height: 1.25; letter-spacing: -.5px; margin: 0; color: var(--color-text); }
20
+ .mdbase-feedback-header button { border: 0; width: 34px; height: 34px; padding: 0; font-size: 23px; color: var(--color-text-muted); }
21
+ .mdbase-feedback-dialog form { display: flex; flex-direction: column; flex: 1; overflow: hidden; margin: 0; }
22
+ .mdbase-feedback-body { flex: 1; overflow-y: auto; padding: 23px 27px; }
23
+ .mdbase-feedback-topics { display: grid; gap: 6px; border: 0; padding: 0; margin: 0 0 23px; }
24
+ .mdbase-feedback-topics legend { margin-bottom: 10px; font-size: 12px; font-weight: 700; }
25
+ .mdbase-feedback-topics label { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--color-border); border-radius: 4px; font-size: 12px; cursor: pointer; }
26
+ .mdbase-feedback-topics label:has(:checked) { border-color: var(--color-accent); background: var(--color-accent-soft); }
27
+ .mdbase-feedback-dialog input:is([type=radio],[type=checkbox]) { accent-color: var(--color-accent); margin: 0; width: 14px; height: 14px; flex-shrink: 0; }
28
+ .mdbase-feedback-field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; font-weight: 700; }
29
+ .mdbase-feedback-field small { margin-left: 6px; font-weight: 400; font-size: 11px; color: var(--color-text-muted); }
30
+ .mdbase-feedback-field :is(input,textarea) { width: 100%; background: var(--color-canvas); border: 1px solid var(--color-border-strong); border-radius: 4px; color: var(--color-text); padding: 10px 11px; font-weight: 400; }
31
+ .mdbase-feedback-field textarea { min-height: 125px; resize: vertical; }
32
+ .mdbase-feedback-field :is(input,textarea)::placeholder { color: var(--color-text-muted); }
33
+ .mdbase-feedback-screenshot { margin: 0 0 20px; }
34
+ .mdbase-feedback-dialog .mdbase-feedback-preview { display: block; position: relative; padding: 0; width: 100%; overflow: hidden; }
35
+ .mdbase-feedback-preview img { display: block; width: 100%; height: 165px; object-fit: contain; }
36
+ .mdbase-feedback-preview span { position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); background: var(--color-canvas); border: 1px solid var(--color-border-strong); border-radius: 4px; padding: 4px 8px; font-size: 11px; white-space: nowrap; }
37
+ .mdbase-feedback-screenshot-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; font-size: 11px; }
38
+ .mdbase-feedback-screenshot-actions label { display: flex; align-items: center; gap: 7px; }
39
+ .mdbase-feedback-upload { position: relative; color: var(--color-accent); cursor: pointer; padding: 6px 0; }
40
+ .mdbase-feedback-upload input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
41
+ .mdbase-feedback-upload:focus-within { outline: 2px solid var(--color-accent); outline-offset: 3px; }
42
+ .mdbase-feedback-check { display: flex; gap: 8px; align-items: center; font-size: 12px; }
43
+ .mdbase-feedback-details > .mdbase-feedback-check { margin: 14px 0 8px; }
44
+ .mdbase-feedback-help { font-size: 11px; line-height: 1.6; color: var(--color-text-muted); margin: 9px 0; }
45
+ .mdbase-feedback-dialog details { font-size: 11px; color: var(--color-text-soft); margin: 12px 0; }
46
+ .mdbase-feedback-dialog summary { cursor: pointer; }
47
+ .mdbase-feedback-dialog pre { font: 11px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; background: var(--color-surface-subtle); padding: 12px; border: 1px solid var(--color-border); border-radius: 4px; }
48
+ .mdbase-feedback-error { color: var(--color-danger); font-size: 12px; }
49
+ .mdbase-feedback-actions { display: flex; justify-content: space-between; align-items: center; gap: 12px; border-top: 1px solid var(--color-border); padding: 18px 27px; flex-shrink: 0; }
50
+ .mdbase-feedback-dialog .mdbase-feedback-primary, .mdbase-feedback-markup .mdbase-feedback-actions .mdbase-feedback-primary { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-on-accent); }
51
+ .mdbase-feedback-dialog .mdbase-feedback-primary:hover, .mdbase-feedback-markup .mdbase-feedback-actions .mdbase-feedback-primary:hover { background: var(--color-accent-strong); }
52
+ .mdbase-feedback-success { padding: 40px 27px; }
53
+ .mdbase-feedback-success h3 { font-size: 25px; line-height: 1.3; margin: 0 0 17px; }
54
+ .mdbase-feedback-success .mdbase-feedback-primary { margin-top: 20px; }
55
+ .mdbase-feedback-markup { width: min(1100px,calc(100vw - 32px)); max-width: none; max-height: calc(100dvh - 32px); padding: 0; }
56
+ .mdbase-feedback-markup .mdbase-dialog-body { max-height: calc(100dvh - 34px); display: flex; flex-direction: column; overflow: hidden; padding: 20px; }
57
+ .mdbase-feedback-tools { display: flex; gap: 7px; flex-wrap: wrap; margin: 10px 0 15px; }
58
+ .mdbase-feedback-tools button[aria-pressed=true] { background: var(--color-selected); border-color: var(--color-accent); }
59
+ .mdbase-feedback-canvas { background: var(--color-surface-subtle); border: 1px solid var(--color-border); padding: 12px; overflow: auto; display: flex; justify-content: center; align-items: center; min-height: 0; }
60
+ .mdbase-feedback-canvas canvas { display: block; max-width: 100%; max-height: calc(100dvh - 345px); width: auto; height: auto; background: white; touch-action: none; cursor: crosshair; }
61
+ .mdbase-feedback-markup .mdbase-feedback-actions { padding: 15px 0 0; }
62
+ @media (max-width: 600px) { .mdbase-feedback-header { padding: 23px; } .mdbase-feedback-body { padding: 20px 23px; } .mdbase-feedback-actions { padding: 16px 23px; } .mdbase-feedback-tools { gap: 4px; } .mdbase-feedback-tools button { font-size: 11px; padding: 6px 8px; } }
package/css/motion.css CHANGED
@@ -1,198 +1,306 @@
1
- /* Animations for the platform mark. Base mark styling lives in brand.css. */
1
+ /*
2
+ * Animations for the Frontmatter mark. Base mark styling lives in brand.css.
3
+ * Colours go through --mark-ink and --mark-accent so the platform mark and the
4
+ * inverted app marks share every animation.
5
+ */
2
6
 
3
- /* Bootstrap: trace once in reading order, then continue as bounded I/O. */
4
- .mdbase-motion-bootstrap .mdbase-mark-segment {
5
- --mdbase-trace-delay: 0ms;
6
- stroke: currentColor;
7
- stroke-width: 3;
8
- stroke-dasharray: 1;
9
- stroke-dashoffset: 1;
10
- vector-effect: non-scaling-stroke;
11
- animation: mdbase-trace-ink 720ms cubic-bezier(0.16, 1, 0.3, 1) var(--mdbase-trace-delay) 1 both;
7
+ /* Each segment's place in reading order, and where assemble scatters it from. */
8
+ .mdbase-mark-segment-1 { --step: 0; --scatter: translate(-18px, -22px) rotate(-24deg); }
9
+ .mdbase-mark-segment-2 { --step: 1; --scatter: translate(6px, -30px) rotate(18deg); }
10
+ .mdbase-mark-segment-3 { --step: 2; --scatter: translate(26px, -18px) rotate(-12deg); }
11
+ .mdbase-mark-segment-4 { --step: 3; --scatter: translate(-30px, 4px) rotate(30deg); }
12
+ .mdbase-mark-segment-5 { --step: 4; --scatter: translate(34px, 6px) rotate(-20deg); }
13
+ .mdbase-mark-segment-6 { --step: 5; --scatter: translate(-24px, 26px) rotate(16deg); }
14
+ .mdbase-mark-segment-7 { --step: 6; --scatter: translate(22px, 30px) rotate(-28deg); }
15
+ .mdbase-mark-segment-8 { --step: 7; --scatter: translate(-14px, 34px) rotate(22deg); }
16
+ .mdbase-mark-segment-9 { --step: 8; --scatter: translate(8px, 40px) rotate(-16deg); }
17
+ .mdbase-mark-segment-10 { --step: 9; --scatter: translate(30px, 24px) rotate(26deg); }
18
+
19
+ /* ---------- Loops ---------- */
20
+
21
+ /* Orbit: the fences run in opposite directions, so the mark reads as turning. */
22
+ .mdbase-motion-orbit .mdbase-mark-fence {
23
+ opacity: 0;
12
24
  }
13
25
 
14
- .mdbase-motion-bootstrap .mdbase-mark-accent {
15
- stroke: var(--color-accent);
16
- animation-name: mdbase-trace-accent;
26
+ .mdbase-motion-orbit .mdbase-mark-track-top {
27
+ animation: mdbase-orbit-right 1.3s linear infinite;
17
28
  }
18
29
 
19
- .mdbase-motion-bootstrap .mdbase-mark-segment-2 { --mdbase-trace-delay: 35ms; }
20
- .mdbase-motion-bootstrap .mdbase-mark-segment-3 { --mdbase-trace-delay: 70ms; }
21
- .mdbase-motion-bootstrap .mdbase-mark-segment-4 { --mdbase-trace-delay: 125ms; }
22
- .mdbase-motion-bootstrap .mdbase-mark-segment-5 { --mdbase-trace-delay: 165ms; }
23
- .mdbase-motion-bootstrap .mdbase-mark-segment-6 { --mdbase-trace-delay: 220ms; }
24
- .mdbase-motion-bootstrap .mdbase-mark-segment-7 { --mdbase-trace-delay: 260ms; }
25
- .mdbase-motion-bootstrap .mdbase-mark-segment-8 { --mdbase-trace-delay: 315ms; }
26
- .mdbase-motion-bootstrap .mdbase-mark-segment-9 { --mdbase-trace-delay: 350ms; }
27
- .mdbase-motion-bootstrap .mdbase-mark-segment-10 { --mdbase-trace-delay: 385ms; }
30
+ .mdbase-motion-orbit .mdbase-mark-track-bottom {
31
+ animation: mdbase-orbit-left 1.3s linear infinite;
32
+ }
28
33
 
29
- .mdbase-motion-bootstrap .mdbase-mark-fence {
30
- animation: mdbase-hide-original-fence 1ms linear 1.12s 1 forwards;
34
+ @keyframes mdbase-orbit-right {
35
+ from { transform: translateX(0); }
36
+ to { transform: translateX(28px); }
31
37
  }
32
38
 
33
- .mdbase-motion-bootstrap .mdbase-mark-conveyor-track {
34
- animation:
35
- mdbase-show-conveyor 1ms linear 1.12s 1 forwards,
36
- mdbase-gap-conveyor 900ms linear 1.12s infinite;
39
+ @keyframes mdbase-orbit-left {
40
+ from { transform: translateX(0); }
41
+ to { transform: translateX(-28px); }
37
42
  }
38
43
 
39
- @keyframes mdbase-trace-ink {
40
- 0% {
41
- fill: transparent;
42
- opacity: 0;
43
- stroke-dashoffset: 1;
44
- stroke-opacity: 1;
45
- }
46
- 10% { opacity: 1; }
47
- 54% {
48
- fill: transparent;
49
- opacity: 1;
50
- stroke-dashoffset: 0;
51
- stroke-opacity: 1;
52
- }
53
- 72%,
54
- 100% {
55
- fill: currentColor;
56
- opacity: 1;
57
- stroke-dashoffset: 0;
58
- stroke-opacity: 0;
59
- }
44
+ /* Scan: a brightness band reads the record line by line. */
45
+ .mdbase-motion-scan :is(.mdbase-mark-fence, .mdbase-mark-row) {
46
+ opacity: 0.35;
47
+ animation: mdbase-scan 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
60
48
  }
61
49
 
62
- @keyframes mdbase-trace-accent {
63
- 0% {
64
- fill: transparent;
65
- opacity: 0;
66
- stroke-dashoffset: 1;
67
- stroke-opacity: 1;
68
- }
69
- 10% { opacity: 1; }
70
- 54% {
71
- fill: transparent;
50
+ .mdbase-motion-scan .mdbase-mark-row-top { animation-delay: 0.14s; }
51
+ .mdbase-motion-scan .mdbase-mark-row-bottom { animation-delay: 0.28s; }
52
+ .mdbase-motion-scan .mdbase-mark-fence-bottom { animation-delay: 0.42s; }
53
+
54
+ @keyframes mdbase-scan {
55
+ 0% { opacity: 0.35; }
56
+ 14% { opacity: 1; }
57
+ 46%, 100% { opacity: 0.35; }
58
+ }
59
+
60
+ /* Bounce: the fence dashes hop like a typing indicator, the bottom fence mirrored. */
61
+ .mdbase-motion-bounce .mdbase-mark-fence .mdbase-mark-segment {
62
+ animation: mdbase-bounce-up 1.2s cubic-bezier(0.45, 0, 0.55, 1) infinite both;
63
+ }
64
+
65
+ .mdbase-motion-bounce .mdbase-mark-fence-bottom .mdbase-mark-segment {
66
+ animation-name: mdbase-bounce-down;
67
+ }
68
+
69
+ .mdbase-motion-bounce :is(.mdbase-mark-segment-2, .mdbase-mark-segment-9) { animation-delay: 0.14s; }
70
+ .mdbase-motion-bounce :is(.mdbase-mark-segment-3, .mdbase-mark-segment-10) { animation-delay: 0.28s; }
71
+
72
+ @keyframes mdbase-bounce-up {
73
+ 0%, 40%, 100% { transform: translateY(0); }
74
+ 20% { transform: translateY(-5px); }
75
+ }
76
+
77
+ @keyframes mdbase-bounce-down {
78
+ 0%, 40%, 100% { transform: translateY(0); }
79
+ 20% { transform: translateY(5px); }
80
+ }
81
+
82
+ /* Stream: records pass through the fences, as in a sync. The rows are clipped to the frame. */
83
+ .mdbase-motion-stream .mdbase-mark-row {
84
+ animation: mdbase-stream 2.2s infinite;
85
+ }
86
+
87
+ .mdbase-motion-stream .mdbase-mark-row-bottom {
88
+ animation-delay: 0.1s;
89
+ }
90
+
91
+ @keyframes mdbase-stream {
92
+ 0%, 30% {
72
93
  opacity: 1;
73
- stroke-dashoffset: 0;
74
- stroke-opacity: 1;
94
+ transform: translateX(0);
95
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
75
96
  }
76
- 72%,
77
- 100% {
78
- fill: var(--color-accent);
79
- opacity: 1;
80
- stroke-dashoffset: 0;
81
- stroke-opacity: 0;
97
+ 48% { opacity: 0; transform: translateX(-44px); }
98
+ 48.01% {
99
+ opacity: 0;
100
+ transform: translateX(44px);
101
+ animation-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
82
102
  }
103
+ 70%, 100% { opacity: 1; transform: translateX(0); }
83
104
  }
84
105
 
85
- @keyframes mdbase-hide-original-fence {
86
- to { opacity: 0; }
106
+ /* Sort: the two fields swap places, one passing behind the other. */
107
+ .mdbase-motion-sort .mdbase-mark-row-top {
108
+ animation: mdbase-sort-top 3s cubic-bezier(0.65, 0, 0.35, 1) infinite;
87
109
  }
88
110
 
89
- @keyframes mdbase-show-conveyor {
90
- to { opacity: 1; }
111
+ .mdbase-motion-sort .mdbase-mark-row-bottom {
112
+ animation: mdbase-sort-bottom 3s cubic-bezier(0.65, 0, 0.35, 1) infinite;
91
113
  }
92
114
 
93
- /* Unfold: the frontmatter boundary opens once to reveal its fields. */
94
- .mdbase-motion-unfold .mdbase-mark-fence-top {
95
- animation: mdbase-unfold-top 760ms cubic-bezier(0.16, 1, 0.3, 1) 1 both;
115
+ @keyframes mdbase-sort-top {
116
+ 0%, 15% { opacity: 1; transform: translateY(0); }
117
+ 27% { opacity: 0.25; }
118
+ 40%, 65% { opacity: 1; transform: translateY(22px); }
119
+ 77% { opacity: 0.25; }
120
+ 90%, 100% { opacity: 1; transform: translateY(0); }
96
121
  }
97
122
 
98
- .mdbase-motion-unfold .mdbase-mark-fence-bottom {
99
- animation: mdbase-unfold-bottom 760ms cubic-bezier(0.16, 1, 0.3, 1) 1 both;
123
+ @keyframes mdbase-sort-bottom {
124
+ 0%, 15% { transform: translateY(0); }
125
+ 40%, 65% { transform: translateY(-22px); }
126
+ 90%, 100% { transform: translateY(0); }
100
127
  }
101
128
 
102
- .mdbase-motion-unfold .mdbase-mark-row {
103
- animation: mdbase-unfold-row 760ms cubic-bezier(0.16, 1, 0.3, 1) 70ms 1 both;
129
+ /* Hop: the highlight moves from one value to the other and back. */
130
+ .mdbase-motion-hop .mdbase-mark-segment-5 {
131
+ animation: mdbase-hop-from 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite;
104
132
  }
105
133
 
106
- @keyframes mdbase-unfold-top {
107
- from { transform: translateY(27px) scaleX(0.62); }
108
- to { transform: translateY(0) scaleX(1); }
134
+ .mdbase-motion-hop .mdbase-mark-segment-7 {
135
+ animation: mdbase-hop-to 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite;
109
136
  }
110
137
 
111
- @keyframes mdbase-unfold-bottom {
112
- from { transform: translateY(-27px) scaleX(0.62); }
113
- to { transform: translateY(0) scaleX(1); }
138
+ @keyframes mdbase-hop-from {
139
+ 0%, 38%, 100% { fill: var(--mark-accent); }
140
+ 50%, 88% { fill: var(--mark-ink); }
114
141
  }
115
142
 
116
- @keyframes mdbase-unfold-row {
117
- from { opacity: 0; transform: scaleX(0.45); }
118
- to { opacity: 1; transform: scaleX(1); }
143
+ @keyframes mdbase-hop-to {
144
+ 0%, 38%, 100% { fill: var(--mark-ink); }
145
+ 50%, 88% { fill: var(--mark-accent); }
119
146
  }
120
147
 
121
- /* Rebalance: two fixed-width rows change structure in opposite directions. */
122
- .mdbase-motion-rebalance .mdbase-mark-segment-4,
123
- .mdbase-motion-rebalance .mdbase-mark-segment-6 {
148
+ /* ---------- Entrances ---------- */
149
+
150
+ /* Keys first: the fences open from their middle dash, keys appear, values extend, and the highlight lands last. */
151
+ .mdbase-motion-keys-first .mdbase-mark-row .mdbase-mark-segment {
124
152
  transform-origin: left center;
125
153
  }
126
154
 
127
- .mdbase-motion-rebalance .mdbase-mark-segment-5,
128
- .mdbase-motion-rebalance .mdbase-mark-segment-7 {
129
- transform-origin: right center;
155
+ .mdbase-motion-keys-first .mdbase-mark-fence .mdbase-mark-segment {
156
+ animation: mdbase-fence-in 420ms cubic-bezier(0.16, 1, 0.3, 1) 100ms 1 both;
130
157
  }
131
158
 
132
- .mdbase-motion-rebalance .mdbase-mark-segment-4 {
133
- animation: mdbase-rebalance-top-key 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
159
+ .mdbase-motion-keys-first :is(.mdbase-mark-segment-2, .mdbase-mark-segment-9) {
160
+ animation-delay: 0ms;
134
161
  }
135
162
 
136
- .mdbase-motion-rebalance .mdbase-mark-segment-5 {
137
- animation: mdbase-rebalance-top-value 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
163
+ .mdbase-motion-keys-first :is(.mdbase-mark-segment-4, .mdbase-mark-segment-6) {
164
+ animation: mdbase-grow 260ms cubic-bezier(0.16, 1, 0.3, 1) 220ms 1 both;
138
165
  }
139
166
 
140
- .mdbase-motion-rebalance .mdbase-mark-segment-6 {
141
- animation: mdbase-rebalance-bottom-key 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
167
+ .mdbase-motion-keys-first .mdbase-mark-segment-7 {
168
+ animation: mdbase-grow 480ms cubic-bezier(0.16, 1, 0.3, 1) 380ms 1 both;
142
169
  }
143
170
 
144
- .mdbase-motion-rebalance .mdbase-mark-segment-7 {
145
- animation: mdbase-rebalance-bottom-value 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite;
171
+ .mdbase-motion-keys-first .mdbase-mark-segment-5 {
172
+ animation:
173
+ mdbase-grow 520ms cubic-bezier(0.16, 1, 0.3, 1) 440ms 1 both,
174
+ mdbase-land 600ms ease 820ms 1 both;
146
175
  }
147
176
 
148
- @keyframes mdbase-rebalance-top-key {
149
- 0%, 15%, 100% { transform: scaleX(1); }
150
- 42%, 72% { transform: scaleX(2.666667); }
177
+ @keyframes mdbase-fence-in {
178
+ from { opacity: 0; transform: scaleX(0); }
179
+ to { opacity: 1; transform: none; }
151
180
  }
152
181
 
153
- @keyframes mdbase-rebalance-top-value {
154
- 0%, 15%, 100% { transform: scaleX(1); }
155
- 42%, 72% { transform: scaleX(0.642857); }
182
+ @keyframes mdbase-grow {
183
+ from { transform: scaleX(0); }
184
+ to { transform: none; }
156
185
  }
157
186
 
158
- @keyframes mdbase-rebalance-bottom-key {
159
- 0%, 15%, 100% { transform: scaleX(1); }
160
- 42%, 72% { transform: scaleX(0.428571); }
187
+ @keyframes mdbase-land {
188
+ from { fill: var(--mark-ink); }
189
+ to { fill: var(--mark-accent); }
161
190
  }
162
191
 
163
- @keyframes mdbase-rebalance-bottom-value {
164
- 0%, 15%, 100% { transform: scaleX(1); }
165
- 42%, 72% { transform: scaleX(1.4); }
192
+ /* Drop: segments fall into place in reading order with a little overshoot. */
193
+ .mdbase-motion-drop .mdbase-mark-segment {
194
+ animation: mdbase-drop 560ms cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--step) * 45ms) 1 both;
166
195
  }
167
196
 
168
- /* Conveyor: repeated fence segments travel left inside exact fence clips. */
169
- .mdbase-motion-conveyor .mdbase-mark-fence {
170
- opacity: 0;
197
+ @keyframes mdbase-drop {
198
+ from { opacity: 0; transform: translateY(-12px); }
199
+ to { opacity: 1; transform: none; }
171
200
  }
172
201
 
173
- .mdbase-motion-conveyor .mdbase-mark-conveyor-track {
174
- opacity: 1;
175
- animation: mdbase-gap-conveyor 900ms linear infinite;
202
+ /* Assemble: scattered pieces settle into the record. */
203
+ .mdbase-motion-assemble .mdbase-mark-segment {
204
+ animation: mdbase-assemble 900ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--step) * 28ms + 28ms) 1 both;
176
205
  }
177
206
 
178
- @keyframes mdbase-gap-conveyor {
179
- from { transform: translateX(0); }
180
- to { transform: translateX(-28px); }
207
+ @keyframes mdbase-assemble {
208
+ from { opacity: 0; transform: var(--scatter) scale(0.7); }
209
+ 40% { opacity: 1; }
210
+ to { opacity: 1; transform: none; }
211
+ }
212
+
213
+ /* ---------- Signals ---------- */
214
+
215
+ /* Saved: the fences press in, the highlight flashes success while it stretches, then settles. */
216
+ .mdbase-signal-saved .mdbase-mark-accent {
217
+ transform-origin: left center;
218
+ animation: mdbase-saved-stretch 640ms cubic-bezier(0.34, 1.56, 0.64, 1) 80ms 1 both;
219
+ }
220
+
221
+ .mdbase-signal-saved .mdbase-mark-fence-top {
222
+ animation: mdbase-press-top 520ms cubic-bezier(0.34, 1.56, 0.64, 1) 1 both;
223
+ }
224
+
225
+ .mdbase-signal-saved .mdbase-mark-fence-bottom {
226
+ animation: mdbase-press-bottom 520ms cubic-bezier(0.34, 1.56, 0.64, 1) 1 both;
227
+ }
228
+
229
+ .mdbase-signal-saved .mdbase-mark-row-bottom {
230
+ animation: mdbase-saved-dim 640ms ease 1 both;
231
+ }
232
+
233
+ @keyframes mdbase-press-top {
234
+ 40% { transform: translateY(4px); }
235
+ to { transform: none; }
236
+ }
237
+
238
+ @keyframes mdbase-press-bottom {
239
+ 40% { transform: translateY(-4px); }
240
+ to { transform: none; }
241
+ }
242
+
243
+ @keyframes mdbase-saved-stretch {
244
+ 35% { fill: var(--color-success); transform: scaleX(0.8); }
245
+ to { transform: none; }
246
+ }
247
+
248
+ @keyframes mdbase-saved-dim {
249
+ 30% { opacity: 0.35; }
250
+ to { opacity: 1; }
251
+ }
252
+
253
+ /* Error: the fields shake once and the highlight turns to danger before recovering. */
254
+ .mdbase-signal-error .mdbase-mark-row {
255
+ animation: mdbase-shake 520ms cubic-bezier(0.36, 0.07, 0.19, 0.97) 1 both;
256
+ }
257
+
258
+ .mdbase-signal-error .mdbase-mark-row-bottom {
259
+ animation-delay: 40ms;
260
+ }
261
+
262
+ .mdbase-signal-error .mdbase-mark-accent {
263
+ animation: mdbase-danger 1.4s ease 1 both;
264
+ }
265
+
266
+ @keyframes mdbase-shake {
267
+ 15% { transform: translateX(-4px); }
268
+ 35% { transform: translateX(4px); }
269
+ 55% { transform: translateX(-2.5px); }
270
+ 75% { transform: translateX(1.5px); }
271
+ to { transform: none; }
272
+ }
273
+
274
+ @keyframes mdbase-danger {
275
+ 0%, 60% { fill: var(--color-danger); }
276
+ to { fill: var(--mark-accent); }
277
+ }
278
+
279
+ /* ---------- Hover ---------- */
280
+
281
+ /* Wave: one ripple runs through the resting mark in reading order. */
282
+ :is(.mdbase-app-switcher, .mdbase-mark-hover-host):hover .mdbase-mark-at-rest .mdbase-mark-segment {
283
+ animation: mdbase-wave 420ms cubic-bezier(0.45, 0, 0.55, 1) calc(var(--step) * 30ms) 1 both;
284
+ }
285
+
286
+ @keyframes mdbase-wave {
287
+ 50% { transform: translateY(-3px); }
181
288
  }
182
289
 
183
290
  @media (prefers-reduced-motion: reduce) {
184
- .mdbase-mark .mdbase-mark-segment,
185
- .mdbase-mark .mdbase-mark-row,
186
- .mdbase-mark .mdbase-mark-fence,
187
- .mdbase-mark .mdbase-mark-conveyor-track {
291
+ .mdbase-mark :is(.mdbase-mark-segment, .mdbase-mark-row, .mdbase-mark-fence, .mdbase-mark-track) {
188
292
  animation: none !important;
189
- filter: none !important;
190
293
  opacity: 1;
191
- stroke: none;
192
294
  transform: none !important;
193
295
  }
194
296
 
195
- .mdbase-mark .mdbase-mark-conveyor-track {
196
- opacity: 0;
297
+ /* Orbit's moving tracks stand in for the fences; with motion off, show the fences. */
298
+ .mdbase-mark .mdbase-mark-track {
299
+ display: none;
300
+ }
301
+
302
+ /* Keep a still cue for a failure, since it carries meaning. */
303
+ .mdbase-signal-error .mdbase-mark-accent {
304
+ fill: var(--color-danger);
197
305
  }
198
306
  }
package/css/screens.css CHANGED
@@ -1,4 +1,4 @@
1
- /* Opening a collection: the wordmark, held still, over one quiet line. */
1
+ /* Opening a collection: the wordmark, scanning, over one quiet line. */
2
2
  .mdbase-opening {
3
3
  width: 100%;
4
4
  height: 100%;