@inkra/react 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.
Files changed (48) hide show
  1. package/README.md +89 -0
  2. package/dist/PencilIcon.d.ts +14 -1
  3. package/dist/PencilIcon.js +88 -52
  4. package/dist/guide/index.d.ts +77 -0
  5. package/dist/guide/index.js +379 -0
  6. package/dist/i18n/catalog.d.ts +1 -1
  7. package/dist/i18n/catalog.js +6 -0
  8. package/dist/image-viewer/index.d.ts +2 -0
  9. package/dist/image-viewer/index.js +28 -28
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +4 -0
  12. package/dist/infinite-scroll/gesture.d.ts +19 -0
  13. package/dist/infinite-scroll/gesture.js +218 -0
  14. package/dist/infinite-scroll/index.d.ts +55 -0
  15. package/dist/infinite-scroll/index.js +543 -0
  16. package/dist/infinite-scroll/model.d.ts +33 -0
  17. package/dist/infinite-scroll/model.js +168 -0
  18. package/dist/internal/iconFill.d.ts +16 -0
  19. package/dist/internal/iconFill.js +275 -0
  20. package/dist/inventory-grid/index.js +13 -3
  21. package/dist/inventory-grid/outline.d.ts +9 -0
  22. package/dist/inventory-grid/outline.js +54 -0
  23. package/dist/loading/index.d.ts +58 -0
  24. package/dist/loading/index.js +188 -0
  25. package/dist/loading/locales.d.ts +15 -0
  26. package/dist/loading/locales.js +34 -0
  27. package/dist/loading/model.d.ts +6 -0
  28. package/dist/loading/model.js +29 -0
  29. package/dist/masonry/index.d.ts +25 -0
  30. package/dist/masonry/index.js +227 -0
  31. package/dist/masonry/layout.d.ts +35 -0
  32. package/dist/masonry/layout.js +67 -0
  33. package/dist/number-ticker/RollingValue.d.ts +6 -0
  34. package/dist/number-ticker/RollingValue.js +10 -0
  35. package/dist/number-ticker/index.d.ts +4 -0
  36. package/dist/number-ticker/index.js +55 -7
  37. package/dist/number-ticker/rolling.d.ts +35 -0
  38. package/dist/number-ticker/rolling.js +87 -0
  39. package/dist/styles/guide.css +202 -0
  40. package/dist/styles/image-viewer.css +20 -0
  41. package/dist/styles/infinite-scroll.css +171 -0
  42. package/dist/styles/inventory-grid.css +20 -8
  43. package/dist/styles/loading.css +348 -0
  44. package/dist/styles/masonry.css +68 -0
  45. package/dist/styles/number-ticker.css +46 -0
  46. package/dist/styles.css +879 -8
  47. package/dist/toast/manager.d.ts +4 -4
  48. package/package.json +26 -1
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PencilText } from '../PencilText.js';
3
+ import { rollMix } from './rolling.js';
4
+ export function RollingValue({ columns, progress, direction }) {
5
+ return _jsx("span", { className: "inkra-number-ticker__reels", dir: "ltr", "data-direction": direction, children: columns.map(column => {
6
+ const width = rollMix(column.fromWidth, column.toWidth, progress);
7
+ return _jsx("span", { className: "inkra-number-ticker__column", "data-place": column.key, "data-digit": column.digit || undefined, "data-departing": column.target === '' || undefined, style: column.digit ? { width: `${width}ch` } : { gridTemplateColumns: `${width}fr` }, children: _jsx("span", { className: "inkra-number-ticker__column-inner", children: column.faces.map(face => _jsx("span", { className: "inkra-number-ticker__face", "data-face": face.text === column.target ? 'to' : 'from', style: { transform: column.digit ? `translateY(${rollMix(face.fromOffset, face.toOffset, progress) * 100}%)` : undefined,
8
+ opacity: rollMix(face.fromOpacity, face.toOpacity, progress) }, children: _jsx(PencilText, { seed: `ticker-reel-${column.key}-${face.text}`, children: face.text }) }, face.text)) }) }, column.key);
9
+ }) });
10
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
2
  import type { NumberTickerCompact, NumberTickerCompactOptions, NumberTickerFormatOptions, NumberTickerLocale, NumberTickerValue } from './model.js';
3
+ export type NumberTickerAnimation = 'count' | 'roll';
4
+ export type NumberTickerRollDirection = 'auto' | 'up' | 'down';
3
5
  export interface NumberTickerRenderDetails {
4
6
  animating: boolean;
5
7
  formattedValue: string;
@@ -19,6 +21,8 @@ export interface NumberTickerProps extends Omit<ComponentPropsWithoutRef<'span'>
19
21
  renderValue?: (value: NumberTickerValue, details: NumberTickerRenderDetails) => ReactNode;
20
22
  getValueLabel?: (value: NumberTickerValue, formattedValue: string) => string;
21
23
  animate?: boolean;
24
+ animation?: NumberTickerAnimation;
25
+ rollDirection?: NumberTickerRollDirection;
22
26
  duration?: number;
23
27
  easing?: (progress: number) => number;
24
28
  showDelta?: boolean;
@@ -5,6 +5,8 @@ import { PencilText } from '../PencilText.js';
5
5
  import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
6
6
  import { canAnimateTickerValue, createNumberFormatter, createTickerFormatter, easeOutCubic, formatDerivedTickerDelta, formatTickerValue, interpolateTickerValue, tickerValueChanged, subtractTickerValues, tickerValueKey, } from './model.js';
7
7
  import { ExactDecimal } from './precision.js';
8
+ import { RollingValue } from './RollingValue.js';
9
+ import { createRollingMotion, rollingTokens, sameRollingTokens } from './rolling.js';
8
10
  function usePrefersReducedMotion() {
9
11
  const [reduced, setReduced] = useState(false);
10
12
  useBrowserLayoutEffect(() => {
@@ -24,7 +26,7 @@ function lettering(content, seed) {
24
26
  function accessibleAffix(content) {
25
27
  return typeof content === 'string' || typeof content === 'number' ? String(content) : '';
26
28
  }
27
- export const NumberTicker = forwardRef(function NumberTicker({ animate = true, className, compact = false, compactOptions, deltaDuration = 1600, deltaFormatOptions, duration = 420, easing = easeOutCubic, formatOptions, getValueLabel, locale: localeProp, prefix, renderDelta, renderValue, showDelta = false, suffix, value, ...props }, forwardedRef) {
29
+ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, animation = 'count', className, compact = false, compactOptions, deltaDuration = 1600, deltaFormatOptions, duration = 420, easing = easeOutCubic, formatOptions, getValueLabel, locale: localeProp, prefix, renderDelta, renderValue, rollDirection = 'auto', showDelta = false, suffix, value, ...props }, forwardedRef) {
28
30
  const { locale: inheritedLocale } = useInkraLocale();
29
31
  const locale = localeProp ?? inheritedLocale;
30
32
  const rootRef = useRef(null);
@@ -36,6 +38,9 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
36
38
  const [announcedValue, setAnnouncedValue] = useState(value);
37
39
  const [animating, setAnimating] = useState(false);
38
40
  const [delta, setDelta] = useState(null);
41
+ const [roll, setRoll] = useState(null);
42
+ const [rollProgress, setRollProgress] = useState(1);
43
+ const rollFrameRef = useRef(null);
39
44
  const reducedMotion = usePrefersReducedMotion();
40
45
  const localeKey = Array.isArray(locale) ? locale.join('\u0000') : locale;
41
46
  const formatOptionsKey = JSON.stringify(formatOptions ?? {});
@@ -45,6 +50,14 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
45
50
  const deltaFormatter = useMemo(() => createTickerFormatter(locale, deltaFormatOptions ?? formatOptions, compact, compactOptions), [localeKey, deltaFormatOptionsKey, compact, compactOptionsKey]);
46
51
  const announcementFormatter = useMemo(() => createNumberFormatter(locale, compact ? { ...formatOptions, notation: 'standard' } : formatOptions), [localeKey, formatOptionsKey, compact]);
47
52
  useImperativeHandle(forwardedRef, () => rootRef.current);
53
+ const rollRequested = animation === 'roll' && !renderValue;
54
+ const finalTokens = useMemo(() => rollRequested ? rollingTokens(formatter.formatToParts(value)) : [], [formatter, value, rollRequested]);
55
+ // Keep exact large values readable without creating thousands of painted reels.
56
+ const useRoll = rollRequested && finalTokens.length <= 128;
57
+ const settledTokensRef = useRef(finalTokens);
58
+ const previousFormatterRef = useRef(formatter);
59
+ const previousUseRollRef = useRef(useRoll);
60
+ const columns = useMemo(() => roll?.columns ?? createRollingMotion(finalTokens, finalTokens, 'up'), [roll, finalTokens]);
48
61
  useBrowserLayoutEffect(() => {
49
62
  const previousTarget = targetRef.current;
50
63
  targetRef.current = value;
@@ -72,22 +85,53 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
72
85
  && typeof window.matchMedia === 'function'
73
86
  && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
74
87
  const from = displayedRef.current;
75
- if (!animate || reduceImmediately || !canAnimateTickerValue(from, value, duration)) {
88
+ const formatChanged = previousFormatterRef.current !== formatter;
89
+ previousFormatterRef.current = formatter;
90
+ const modeChanged = previousUseRollRef.current !== useRoll;
91
+ previousUseRollRef.current = useRoll;
92
+ if (!animate || reduceImmediately || formatChanged || modeChanged
93
+ || (useRoll && !rollFrameRef.current && sameRollingTokens(settledTokensRef.current, finalTokens))
94
+ || (rollRequested && (!useRoll || settledTokensRef.current.length > 128))
95
+ || !canAnimateTickerValue(from, value, duration)) {
76
96
  displayedRef.current = value;
77
97
  setDisplayedValue(value);
78
98
  setAnnouncedValue(value);
79
99
  setAnimating(false);
100
+ setRoll(null);
101
+ setRollProgress(1);
102
+ settledTokensRef.current = finalTokens;
103
+ rollFrameRef.current = null;
80
104
  return undefined;
81
105
  }
106
+ if (useRoll) {
107
+ const direction = rollDirection === 'auto'
108
+ ? (new ExactDecimal(String(value)).gt(String(from)) ? 'up' : 'down') : rollDirection;
109
+ const nextColumns = createRollingMotion(settledTokensRef.current, finalTokens, direction, rollFrameRef.current ?? undefined);
110
+ setRoll({ columns: nextColumns, direction });
111
+ setRollProgress(0);
112
+ rollFrameRef.current = { columns: nextColumns, progress: 0 };
113
+ }
114
+ else {
115
+ setRoll(null);
116
+ rollFrameRef.current = null;
117
+ }
82
118
  let startTime = null;
83
119
  setAnimating(true);
84
120
  const tick = (time) => {
85
121
  if (startTime === null)
86
122
  startTime = time;
87
123
  const progress = Math.min(1, Math.max(0, (time - startTime) / duration));
88
- const next = interpolateTickerValue(from, value, easing(progress));
124
+ const eased = easing(progress);
125
+ const bounded = Number.isFinite(eased) ? Math.min(1, Math.max(0, eased)) : progress;
126
+ const next = interpolateTickerValue(from, value, bounded);
89
127
  displayedRef.current = next;
90
- setDisplayedValue(next);
128
+ if (useRoll) {
129
+ setRollProgress(bounded);
130
+ if (rollFrameRef.current)
131
+ rollFrameRef.current.progress = bounded;
132
+ }
133
+ else
134
+ setDisplayedValue(next);
91
135
  if (progress < 1) {
92
136
  frameRef.current = requestAnimationFrame(tick);
93
137
  }
@@ -97,6 +141,10 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
97
141
  setDisplayedValue(value);
98
142
  setAnnouncedValue(value);
99
143
  setAnimating(false);
144
+ setRoll(null);
145
+ setRollProgress(1);
146
+ settledTokensRef.current = finalTokens;
147
+ rollFrameRef.current = null;
100
148
  }
101
149
  };
102
150
  frameRef.current = requestAnimationFrame(tick);
@@ -106,7 +154,7 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
106
154
  frameRef.current = null;
107
155
  }
108
156
  };
109
- }, [animate, deltaDuration, duration, easing, reducedMotion, showDelta, value]);
157
+ }, [animate, deltaDuration, duration, easing, reducedMotion, showDelta, value, useRoll, rollRequested, rollDirection, formatter]);
110
158
  useEffect(() => () => {
111
159
  if (frameRef.current !== null)
112
160
  cancelAnimationFrame(frameRef.current);
@@ -120,7 +168,7 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
120
168
  .filter(Boolean).join(' ');
121
169
  const accessibleLabel = getValueLabel?.(announcedValue, announcedFormattedValue) ?? defaultAccessibleLabel;
122
170
  const valueContent = renderValue?.(displayedValue, { animating, formattedValue })
123
- ?? (animating
171
+ ?? (useRoll ? _jsx(RollingValue, { columns: columns, progress: roll ? rollProgress : 1, direction: roll?.direction ?? 'up' }) : animating
124
172
  ? _jsx("span", { className: "inkra-number-ticker__native-value", children: formattedValue })
125
173
  : _jsx(PencilText, { seed: `number-ticker-${finalFormattedValue}`, children: finalFormattedValue }));
126
174
  const deltaContent = delta === null ? null : (() => {
@@ -133,6 +181,6 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
133
181
  })();
134
182
  const classes = ['inkra-number-ticker', animating && 'inkra-number-ticker--animating', className]
135
183
  .filter(Boolean).join(' ');
136
- return _jsxs("span", { ...props, className: classes, ref: rootRef, "data-animating": animating || undefined, children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-number-ticker__visual", children: [prefix !== undefined && _jsx("span", { className: "inkra-number-ticker__prefix", children: lettering(prefix, 'number-ticker-prefix') }), _jsx("span", { className: "inkra-number-ticker__value", children: valueContent }), suffix !== undefined && _jsx("span", { className: "inkra-number-ticker__suffix", children: lettering(suffix, 'number-ticker-suffix') }), deltaContent !== null && _jsx("span", { className: "inkra-number-ticker__delta", "data-direction": new ExactDecimal(String(delta)).lt(0) ? 'decrease' : 'increase', children: deltaContent })] }), _jsx("span", { className: "inkra-number-ticker__announcement", "aria-atomic": "true", "aria-live": "polite", children: accessibleLabel })] });
184
+ return _jsxs("span", { ...props, className: classes, ref: rootRef, "data-animating": animating || undefined, "data-animation": animation, children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-number-ticker__visual", children: [prefix !== undefined && _jsx("span", { className: "inkra-number-ticker__prefix", children: lettering(prefix, 'number-ticker-prefix') }), _jsx("span", { className: "inkra-number-ticker__value", children: valueContent }), suffix !== undefined && _jsx("span", { className: "inkra-number-ticker__suffix", children: lettering(suffix, 'number-ticker-suffix') }), deltaContent !== null && _jsx("span", { className: "inkra-number-ticker__delta", "data-direction": new ExactDecimal(String(delta)).lt(0) ? 'decrease' : 'increase', children: deltaContent })] }), _jsx("span", { className: "inkra-number-ticker__announcement", "aria-atomic": "true", "aria-live": "polite", children: accessibleLabel })] });
137
185
  });
138
186
  export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, numberTickerUnits, } from './model.js';
@@ -0,0 +1,35 @@
1
+ export interface RollingToken {
2
+ key: string;
3
+ text: string;
4
+ digit: boolean;
5
+ }
6
+ export declare function sameRollingTokens(from: readonly RollingToken[], to: readonly RollingToken[]): boolean;
7
+ export interface RollingColumn {
8
+ key: string;
9
+ from: string;
10
+ to: string;
11
+ digit: boolean;
12
+ }
13
+ export interface RollingFace {
14
+ text: string;
15
+ fromOffset: number;
16
+ toOffset: number;
17
+ fromOpacity: number;
18
+ toOpacity: number;
19
+ }
20
+ export interface RollingMotionColumn {
21
+ key: string;
22
+ digit: boolean;
23
+ target: string;
24
+ fromWidth: number;
25
+ toWidth: number;
26
+ faces: RollingFace[];
27
+ }
28
+ export interface RollingFrame {
29
+ columns: RollingMotionColumn[];
30
+ progress: number;
31
+ }
32
+ export declare const rollMix: (from: number, to: number, progress: number) => number;
33
+ export declare function createRollingMotion(from: RollingToken[], to: RollingToken[], direction: 'up' | 'down', previous?: RollingFrame): RollingMotionColumn[];
34
+ export declare function rollingTokens(parts: Intl.NumberFormatPart[]): RollingToken[];
35
+ export declare function rollingColumns(from: RollingToken[], to: RollingToken[]): RollingColumn[];
@@ -0,0 +1,87 @@
1
+ export function sameRollingTokens(from, to) {
2
+ return from.length === to.length && from.every((token, index) => {
3
+ const next = to[index];
4
+ return token.key === next.key && token.text === next.text && token.digit === next.digit;
5
+ });
6
+ }
7
+ export const rollMix = (from, to, progress) => from + (to - from) * progress;
8
+ export function createRollingMotion(from, to, direction, previous) {
9
+ const sign = direction === 'up' ? -1 : 1;
10
+ const snapshot = previous?.columns.map(column => ({
11
+ key: column.key, digit: column.digit,
12
+ width: rollMix(column.fromWidth, column.toWidth, previous.progress),
13
+ faces: column.faces.map(face => ({
14
+ text: face.text,
15
+ offset: rollMix(face.fromOffset, face.toOffset, previous.progress),
16
+ opacity: rollMix(face.fromOpacity, face.toOpacity, previous.progress),
17
+ })).filter(face => Math.abs(face.offset) < 1 && face.opacity > 0.01),
18
+ })) ?? from.map(token => ({ key: token.key, digit: token.digit, width: 1, faces: [{ text: token.text, offset: 0, opacity: 1 }] }));
19
+ const sources = new Map(snapshot.map(column => [column.key, column]));
20
+ const sourceTokens = snapshot.filter(column => column.width > 0 || column.faces.length > 0)
21
+ .map(column => ({ key: column.key, digit: column.digit, text: column.faces[0]?.text ?? '' }));
22
+ return rollingColumns(sourceTokens, to).map(column => {
23
+ const source = sources.get(column.key);
24
+ const faces = [...(source?.faces ?? [])];
25
+ if (column.to !== '' && !faces.some(face => face.text === column.to)) {
26
+ faces.push({ text: column.to, offset: column.digit ? -sign : 0, opacity: column.digit ? 1 : 0 });
27
+ }
28
+ return {
29
+ key: column.key, digit: column.digit, target: column.to,
30
+ fromWidth: source?.width ?? 0, toWidth: column.to === '' ? 0 : 1,
31
+ // Existing faces continue from their actual offset on a rapid retarget.
32
+ faces: faces.map(face => ({ text: face.text, fromOffset: face.offset,
33
+ toOffset: face.text === column.to ? 0 : column.digit ? sign : 0,
34
+ fromOpacity: face.opacity, toOpacity: face.text === column.to || column.digit ? 1 : 0,
35
+ })),
36
+ };
37
+ });
38
+ }
39
+ // Align integer places from the right and fractional places from the decimal.
40
+ // Intl parts keep signs, grouping, currency and compact units out of digit reels.
41
+ export function rollingTokens(parts) {
42
+ const remaining = new Map();
43
+ for (const part of parts) {
44
+ if (part.type === 'integer' || part.type === 'exponentInteger') {
45
+ remaining.set(part.type, (remaining.get(part.type) ?? 0) + Array.from(part.value).length);
46
+ }
47
+ }
48
+ const occurrences = new Map();
49
+ return parts.flatMap((part, index) => {
50
+ if (part.type === 'integer' || part.type === 'exponentInteger') {
51
+ return Array.from(part.value, text => {
52
+ const position = (remaining.get(part.type) ?? 1) - 1;
53
+ remaining.set(part.type, position);
54
+ return { key: `${part.type}-${position}`, text, digit: true };
55
+ });
56
+ }
57
+ if (part.type === 'fraction') {
58
+ return Array.from(part.value, (text, position) => ({ key: `fraction-${position}`, text, digit: true }));
59
+ }
60
+ const identity = part.type === 'literal'
61
+ ? `literal-${part.value}-${parts.slice(index + 1).find(next => next.type !== 'literal')?.type ?? 'end'}`
62
+ : part.type;
63
+ const occurrence = occurrences.get(identity) ?? 0;
64
+ occurrences.set(identity, occurrence + 1);
65
+ const key = part.type === 'group' ? `group-${remaining.get('integer')}` : `${identity}-${occurrence}`;
66
+ return [{ key, text: part.value, digit: false }];
67
+ });
68
+ }
69
+ export function rollingColumns(from, to) {
70
+ const old = new Map(from.map(token => [token.key, token]));
71
+ const next = new Map(to.map(token => [token.key, token]));
72
+ const result = to.map(token => ({
73
+ key: token.key, from: old.get(token.key)?.text ?? '', to: token.text, digit: token.digit,
74
+ }));
75
+ // Departing places retain their old order until their width reaches zero.
76
+ for (let index = from.length - 1; index >= 0; index -= 1) {
77
+ const token = from[index];
78
+ if (next.has(token.key))
79
+ continue;
80
+ const following = from[index + 1];
81
+ const insertion = following ? result.findIndex(column => column.key === following.key) : -1;
82
+ result.splice(insertion < 0 ? result.length : insertion, 0, {
83
+ key: token.key, from: token.text, to: '', digit: token.digit,
84
+ });
85
+ }
86
+ return result;
87
+ }
@@ -0,0 +1,202 @@
1
+ .inkra-guide__spotlight,
2
+ .inkra-guide__backdrop {
3
+ inset: 0;
4
+ pointer-events: none;
5
+ position: fixed;
6
+ z-index: calc(var(--inkra-z-modal, 30) + var(--inkra-floating-layer-offset, 0));
7
+ }
8
+
9
+ .inkra-guide__backdrop {
10
+ background: var(--inkra-overlay);
11
+ }
12
+
13
+ .inkra-guide__spotlight {
14
+ box-shadow: 0 0 0 100vmax var(--inkra-overlay);
15
+ overflow: visible;
16
+ }
17
+
18
+ .inkra-guide__spotlight > .inkra-pencil-surface__ink,
19
+ .inkra-guide__spotlight > .inkra-pencil-surface__border {
20
+ border-radius: var(--inkra-guide-highlight-radius, 8px);
21
+ }
22
+
23
+ .inkra-guide__content {
24
+ max-height: min(70dvh, var(--radix-popover-content-available-height, 70dvh));
25
+ max-width: min(24rem, calc(100dvw - var(--inkra-space-6)));
26
+ outline: 0;
27
+ transform-origin: var(--radix-popover-content-transform-origin);
28
+ width: min(22rem, calc(100dvw - var(--inkra-space-6)));
29
+ z-index: calc(var(--inkra-z-modal, 30) + var(--inkra-floating-layer-offset, 0) + 1);
30
+ }
31
+
32
+ .inkra-guide__content:focus-visible {
33
+ outline: var(--inkra-focus-width) solid var(--inkra-focus);
34
+ outline-offset: var(--inkra-focus-offset);
35
+ }
36
+
37
+ .inkra-guide__skip-layer {
38
+ inset: 0;
39
+ pointer-events: none;
40
+ position: fixed;
41
+ z-index: calc(var(--inkra-z-modal, 30) + var(--inkra-floating-layer-offset, 0) + 2);
42
+ }
43
+
44
+ .inkra-guide__skip {
45
+ color: var(--inkra-ink-muted);
46
+ opacity: .72;
47
+ pointer-events: auto;
48
+ position: absolute;
49
+ }
50
+
51
+ .inkra-guide__skip:is(:hover, :focus-visible) {
52
+ color: var(--inkra-ink);
53
+ opacity: 1;
54
+ }
55
+
56
+ .inkra-guide__skip[data-placement^="top"] {
57
+ inset-block-start: max(var(--inkra-space-4), env(safe-area-inset-top));
58
+ }
59
+
60
+ .inkra-guide__skip[data-placement^="bottom"] {
61
+ inset-block-end: max(var(--inkra-space-4), env(safe-area-inset-bottom));
62
+ }
63
+
64
+ .inkra-guide__skip[data-placement$="start"] {
65
+ inset-inline-start: max(var(--inkra-space-4), env(safe-area-inset-left));
66
+ }
67
+
68
+ .inkra-guide__skip[data-placement$="end"] {
69
+ inset-inline-end: max(var(--inkra-space-4), env(safe-area-inset-right));
70
+ }
71
+
72
+ .inkra-guide__surface {
73
+ color: var(--inkra-ink);
74
+ display: block;
75
+ max-height: inherit;
76
+ width: 100%;
77
+ }
78
+
79
+ .inkra-guide__panel {
80
+ box-sizing: border-box;
81
+ display: grid;
82
+ gap: var(--inkra-space-4);
83
+ max-height: inherit;
84
+ overflow: auto;
85
+ overflow-wrap: anywhere;
86
+ overscroll-behavior: contain;
87
+ padding: var(--inkra-space-5);
88
+ }
89
+
90
+ .inkra-guide__header,
91
+ .inkra-guide__footer,
92
+ .inkra-guide__navigation {
93
+ align-items: center;
94
+ display: flex;
95
+ gap: var(--inkra-space-2);
96
+ }
97
+
98
+ .inkra-guide__header {
99
+ align-items: flex-start;
100
+ }
101
+
102
+ .inkra-guide__heading {
103
+ display: grid;
104
+ flex: 1;
105
+ gap: var(--inkra-space-1);
106
+ min-width: 0;
107
+ }
108
+
109
+ .inkra-guide__progress {
110
+ color: var(--inkra-ink-subtle);
111
+ font-family: var(--inkra-font-body);
112
+ font-size: var(--inkra-text-sm);
113
+ line-height: var(--inkra-line-ui);
114
+ }
115
+
116
+ .inkra-guide__title {
117
+ font-family: var(--inkra-font-display);
118
+ font-size: var(--inkra-text-xl);
119
+ font-weight: var(--inkra-weight-regular);
120
+ line-height: var(--inkra-line-ui);
121
+ margin: 0;
122
+ }
123
+
124
+ .inkra-guide__description {
125
+ color: var(--inkra-ink-muted);
126
+ font-family: var(--inkra-font-body);
127
+ font-size: var(--inkra-text-md);
128
+ line-height: var(--inkra-line-body);
129
+ }
130
+
131
+ .inkra-guide__close {
132
+ flex: none;
133
+ }
134
+
135
+ .inkra-guide__footer {
136
+ flex-wrap: wrap;
137
+ justify-content: space-between;
138
+ }
139
+
140
+ .inkra-guide__footer > :empty {
141
+ display: none;
142
+ }
143
+
144
+ .inkra-guide__navigation {
145
+ flex-wrap: wrap;
146
+ justify-content: flex-end;
147
+ margin-inline-start: auto;
148
+ }
149
+
150
+ .inkra-guide__footer .inkra-button {
151
+ max-width: 100%;
152
+ }
153
+
154
+ @media (max-width: 30rem) {
155
+ .inkra-guide__content {
156
+ max-height: calc(100dvh - var(--inkra-space-4));
157
+ max-width: calc(100dvw - var(--inkra-space-4));
158
+ width: calc(100dvw - var(--inkra-space-4));
159
+ }
160
+
161
+ .inkra-guide__panel {
162
+ padding: var(--inkra-space-4);
163
+ }
164
+
165
+ .inkra-guide__footer,
166
+ .inkra-guide__navigation {
167
+ width: 100%;
168
+ }
169
+
170
+ .inkra-guide__footer > .inkra-button,
171
+ .inkra-guide__navigation > .inkra-button {
172
+ flex: 1 1 auto;
173
+ }
174
+ }
175
+
176
+ @media (prefers-reduced-motion: reduce) {
177
+ .inkra-guide__content {
178
+ animation: none !important;
179
+ }
180
+ }
181
+
182
+ @media (forced-colors: active) {
183
+ .inkra-guide__backdrop {
184
+ background: CanvasText;
185
+ opacity: 0.25;
186
+ }
187
+
188
+ .inkra-guide__spotlight {
189
+ box-shadow: 0 0 0 100vmax color-mix(in srgb, CanvasText 25%, transparent);
190
+ outline: 2px solid Highlight;
191
+ }
192
+
193
+ .inkra-guide__surface {
194
+ background: Canvas;
195
+ border: 1px solid CanvasText;
196
+ }
197
+
198
+ .inkra-guide__skip {
199
+ color: ButtonText;
200
+ opacity: 1;
201
+ }
202
+ }
@@ -79,6 +79,7 @@
79
79
  }
80
80
 
81
81
  .inkra-image-viewer__body {
82
+ position: relative;
82
83
  display: grid;
83
84
  flex: 1 1 auto;
84
85
  min-block-size: var(--inkra-touch-target);
@@ -226,6 +227,24 @@
226
227
  padding: var(--inkra-focus-offset);
227
228
  position: relative;
228
229
  scrollbar-gutter: auto;
230
+ pointer-events: auto;
231
+ }
232
+
233
+ .inkra-image-viewer__floating-tools {
234
+ position: absolute;
235
+ inset-inline: max(var(--inkra-space-3), env(safe-area-inset-left)) max(var(--inkra-space-3), env(safe-area-inset-right));
236
+ inset-block-start: max(var(--inkra-space-3), env(safe-area-inset-top));
237
+ display: flex;
238
+ justify-content: center;
239
+ padding-inline: var(--inkra-touch-target);
240
+ pointer-events: none;
241
+ z-index: 6;
242
+ }
243
+
244
+ .inkra-image-viewer__floating-tools[data-position="bottom"] {
245
+ inset-block-start: auto;
246
+ inset-block-end: max(var(--inkra-space-3), env(safe-area-inset-bottom));
247
+ padding-inline: 0;
229
248
  }
230
249
 
231
250
  .inkra-image-viewer__video-navigation {
@@ -345,6 +364,7 @@
345
364
  @media print {
346
365
  .inkra-image-viewer__close-control,
347
366
  .inkra-image-viewer__navigation,
367
+ .inkra-image-viewer__floating-tools,
348
368
  .inkra-image-viewer__bottom { display: none; }
349
369
  }
350
370