marqy 0.0.8 → 1.0.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 (93) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.vscode/settings.json +3 -0
  3. package/README.md +43 -12
  4. package/dist/marqy.css +131 -0
  5. package/dist/marqy.d.ts +4 -3
  6. package/dist/marqy.esm.js +1 -1
  7. package/dist/marqy.esm.js.map +1 -1
  8. package/dist/marqy.js +1 -1
  9. package/dist/marqy.js.map +1 -1
  10. package/dist/marqy.umd.js +1 -1
  11. package/dist/marqy.umd.js.map +1 -1
  12. package/examples/.astro/content-assets.mjs +1 -0
  13. package/examples/.astro/content-modules.mjs +1 -0
  14. package/examples/.astro/content.d.ts +154 -0
  15. package/examples/.astro/data-store.json +1 -0
  16. package/examples/.astro/fonts/font-akkurat-mono-500-normal-40f4e0ea0b54adc5.woff2 +0 -0
  17. package/examples/.astro/fonts/font-akkurat-mono-700-normal-b9f40f1447b587bd.woff +0 -0
  18. package/examples/.astro/fonts/font-grtsk-300-normal-8ef14ca23be6893c.woff2 +0 -0
  19. package/examples/.astro/fonts/font-grtsk-400-normal-13ba8954073ee64a.woff2 +0 -0
  20. package/examples/.astro/fonts/font-grtsk-500-normal-d214468daf16245f.woff2 +0 -0
  21. package/examples/.astro/fonts/font-grtsk-wide-500-normal-6f8c2658b9c2ba8e.woff2 +0 -0
  22. package/examples/.astro/fonts/font-grtsk-wide-700-normal-17406f09c7b68ca3.woff2 +0 -0
  23. package/examples/.astro/fonts.d.ts +4 -0
  24. package/examples/.astro/settings.json +5 -0
  25. package/examples/.astro/types.d.ts +3 -0
  26. package/examples/.yarnrc +1 -0
  27. package/examples/astro.config.mjs +101 -0
  28. package/examples/package.json +30 -0
  29. package/examples/public/apple-touch-icon.png +0 -0
  30. package/examples/public/favicon.ico +0 -0
  31. package/examples/public/favicon.svg +4 -0
  32. package/examples/public/marqy-logo.svg +35 -0
  33. package/examples/src/assets/baggu.jpg +0 -0
  34. package/examples/src/assets/bluesky.jpg +0 -0
  35. package/examples/src/assets/clams-lol.jpg +0 -0
  36. package/examples/src/assets/coconut-club-2.jpg +0 -0
  37. package/examples/src/assets/coconut-club.jpg +0 -0
  38. package/examples/src/assets/lucci.jpg +0 -0
  39. package/examples/src/assets/phila-museum.jpg +0 -0
  40. package/examples/src/assets/red-rooster.jpg +0 -0
  41. package/examples/src/assets/sanity.jpg +0 -0
  42. package/examples/src/assets/usal.jpg +0 -0
  43. package/examples/src/assets/vacation.jpg +0 -0
  44. package/examples/src/components/CopyButton.tsx +26 -0
  45. package/examples/src/components/Divider.tsx +3 -0
  46. package/examples/src/components/Footer.astro +7 -0
  47. package/examples/src/components/InUse.astro +89 -0
  48. package/examples/src/components/InUseExamples.tsx +60 -0
  49. package/examples/src/components/MarqyCredits.tsx +33 -0
  50. package/examples/src/components/MarqyHero.tsx +77 -0
  51. package/examples/src/components/MarqyInteractiveDemo.tsx +46 -0
  52. package/examples/src/components/MarqyPlayground.tsx +496 -0
  53. package/examples/src/components/MarqySpeedDemo.tsx +34 -0
  54. package/examples/src/components/MarqyVerticalDemo.tsx +44 -0
  55. package/examples/src/components/Playground.astro +11 -0
  56. package/examples/src/components/QuickStart.astro +59 -0
  57. package/examples/src/components/SectionHeadline.tsx +41 -0
  58. package/examples/src/components/Tile.tsx +32 -0
  59. package/examples/src/content.config.ts +3 -0
  60. package/examples/src/fonts/Akkurat-Mono-Bold.woff +0 -0
  61. package/examples/src/fonts/Akkurat-Mono.woff +0 -0
  62. package/examples/src/fonts/Akkurat-Mono.woff2 +0 -0
  63. package/examples/src/fonts/Grtsk-Peta-Light.woff +0 -0
  64. package/examples/src/fonts/Grtsk-Peta-Light.woff2 +0 -0
  65. package/examples/src/fonts/Grtsk-Peta-Medium.woff +0 -0
  66. package/examples/src/fonts/Grtsk-Peta-Medium.woff2 +0 -0
  67. package/examples/src/fonts/Grtsk-Peta-Regular.woff +0 -0
  68. package/examples/src/fonts/Grtsk-Peta-Regular.woff2 +0 -0
  69. package/examples/src/fonts/Grtsk-Tera-Bold.woff +0 -0
  70. package/examples/src/fonts/Grtsk-Tera-Bold.woff2 +0 -0
  71. package/examples/src/fonts/Grtsk-Tera-Medium.woff +0 -0
  72. package/examples/src/fonts/Grtsk-Tera-Medium.woff2 +0 -0
  73. package/examples/src/layouts/layout.astro +107 -0
  74. package/examples/src/lib/cn.ts +16 -0
  75. package/examples/src/lib/helpers.ts +13 -0
  76. package/examples/src/lib/hooks/useCopyText.ts +47 -0
  77. package/examples/src/pages/index.astro +56 -0
  78. package/examples/src/styles/_base.css +155 -0
  79. package/examples/src/styles/_components.css +123 -0
  80. package/examples/src/styles/_utilities.css +23 -0
  81. package/examples/src/styles/app.css +397 -0
  82. package/examples/tsconfig.json +3 -0
  83. package/examples/yarn.lock +3316 -0
  84. package/marqy.css +131 -0
  85. package/package.json +26 -12
  86. package/src/marqy.tsx +63 -53
  87. package/tsconfig.json +5 -2
  88. package/dist/marqy.modern.js +0 -2
  89. package/dist/marqy.modern.js.map +0 -1
  90. package/examples/index.css +0 -96
  91. package/examples/index.html +0 -15
  92. package/examples/index.js +0 -62
  93. package/styles.css +0 -65
@@ -0,0 +1,32 @@
1
+ import { useState } from 'react'
2
+ import { cn } from '../lib/cn'
3
+
4
+ const MAX = 1
5
+
6
+ export default function Tile({ value }: { value?: string }) {
7
+ const [variant, setVariant] = useState(0)
8
+
9
+ const randomize = () => {
10
+ setVariant((v) => {
11
+ let next = v
12
+ while (next === v) next = Math.floor(Math.random() * (MAX + 1))
13
+ return next
14
+ })
15
+ }
16
+
17
+ return (
18
+ <span
19
+ onMouseEnter={randomize}
20
+ onTouchStart={randomize}
21
+ data-variant={variant}
22
+ className={cn(
23
+ 'block text-jumbo uppercase text-center py-[0.15em] border-b border-white text-[clamp(2rem,18vw,40rem)] cursor-pointer select-none',
24
+ "hover:data-[variant='0']:bg-neon/40 hover:data-[variant='0']:text-neon",
25
+ "hover:data-[variant='1']:bg-purple/40 hover:data-[variant='1']:text-purple",
26
+ 'transition-colors ease-out duration-1500 hover:duration-0',
27
+ )}
28
+ >
29
+ {value}
30
+ </span>
31
+ )
32
+ }
@@ -0,0 +1,3 @@
1
+ import { defineCollection } from 'astro:content'
2
+
3
+ export const collections = {}
@@ -0,0 +1,107 @@
1
+ ---
2
+ import { Font } from 'astro:assets'
3
+ import '../styles/app.css'
4
+ import 'react-resizable/css/styles.css'
5
+
6
+ import Footer from '../components/Footer.astro'
7
+
8
+ interface Props {
9
+ title?: string
10
+ }
11
+
12
+ const { title = 'MARQY: High-fidelity loops for modern interfaces' } = Astro.props
13
+ ---
14
+
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8" />
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
19
+ <title>{title}</title>
20
+ <Font cssVariable="--font-akkurat-mono" preload />
21
+ <Font cssVariable="--font-grtsk" preload />
22
+ <Font cssVariable="--font-grtsk-wide" preload />
23
+
24
+ <link rel="icon" href="/favicon.ico" sizes="32x32">
25
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
26
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
27
+ <script>
28
+ (async function () {
29
+ const TEXT = 'MARQY/';
30
+ const SIZE = 32;
31
+ const SPEED = 0.25;
32
+ const BG = '#000000';
33
+ const COLOR = '#ffffff';
34
+ const FAVICON_UPDATE_INTERVAL = 1000 / 15; // ~15fps for favicon updates
35
+
36
+ const canvas = document.createElement('canvas');
37
+ canvas.width = SIZE;
38
+ canvas.height = SIZE;
39
+ const ctx = canvas.getContext('2d');
40
+
41
+ if (!ctx || !canvas.toDataURL) return;
42
+
43
+ await document.fonts.ready;
44
+ const match = [...document.fonts].find(
45
+ (f) => f.family.includes('GrtskTera') && !f.family.includes('fallback')
46
+ );
47
+ const fontName = match ? match.family : 'monospace';
48
+ const FONT = `500 14px ${fontName}`;
49
+
50
+ ctx.font = FONT;
51
+ const textWidth = ctx.measureText(TEXT).width;
52
+
53
+ let x = 0;
54
+ let rafId: number | null = null;
55
+ let lastFaviconUpdate = 0;
56
+
57
+ let link = document.querySelector<HTMLLinkElement>("link[rel~='icon'][type='image/svg+xml']");
58
+ if (!link) {
59
+ link = document.createElement('link');
60
+ link.rel = 'icon';
61
+ document.head.appendChild(link);
62
+ }
63
+
64
+ function draw(timestamp: number) {
65
+ if (!ctx || !link) return;
66
+
67
+ x -= SPEED;
68
+ if (x <= -textWidth) x = 0;
69
+
70
+ ctx.fillStyle = BG;
71
+ ctx.fillRect(0, 0, SIZE, SIZE);
72
+
73
+ ctx.font = FONT;
74
+ ctx.fillStyle = COLOR;
75
+ ctx.textBaseline = 'middle';
76
+
77
+ // draw twice side by side so the loop is seamless
78
+ ctx.fillText(TEXT, x, SIZE / 2);
79
+ ctx.fillText(TEXT, x + textWidth, SIZE / 2);
80
+
81
+ // only push to favicon at ~15fps
82
+ if (timestamp - lastFaviconUpdate >= FAVICON_UPDATE_INTERVAL) {
83
+ link.href = canvas.toDataURL('image/png');
84
+ lastFaviconUpdate = timestamp;
85
+ }
86
+
87
+ rafId = requestAnimationFrame(draw);
88
+ }
89
+
90
+ document.addEventListener('visibilitychange', () => {
91
+ if (document.hidden) {
92
+ if (rafId !== null) cancelAnimationFrame(rafId);
93
+ } else {
94
+ rafId = requestAnimationFrame(draw);
95
+ }
96
+ });
97
+
98
+ rafId = requestAnimationFrame(draw);
99
+ })();
100
+ </script>
101
+ </head>
102
+ <body>
103
+ <slot />
104
+
105
+ <Footer />
106
+ </body>
107
+ </html>
@@ -0,0 +1,16 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { extendTailwindMerge } from 'tailwind-merge'
3
+
4
+ const twMerge = extendTailwindMerge({
5
+ extend: {
6
+ classGroups: {
7
+ 'font-size': [
8
+ {
9
+ text: ['p0', 'p1', 'p2', 'p3', 'p4', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
10
+ },
11
+ ],
12
+ },
13
+ },
14
+ })
15
+
16
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(...inputs))
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pads a number with leading zeros to a specified size.
3
+ *
4
+ * @param num - The number to pad.
5
+ * @param size - The desired length of the padded number.
6
+ * @returns The padded number as a string.
7
+ */
8
+ export function zeroPad(num: number, size: number): string {
9
+ let padNum = num.toString()
10
+ while (padNum.length < size) padNum = '0' + padNum
11
+
12
+ return padNum
13
+ }
@@ -0,0 +1,47 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ // copy text to clipboard hook
4
+ type UseCopyTextReturn = readonly [
5
+ isCopying: boolean,
6
+ copyText: (text: string) => Promise<void>,
7
+ ]
8
+
9
+ export function useCopyText(): UseCopyTextReturn {
10
+ const [isCopying, setIsCopying] = useState(false)
11
+
12
+ const handleCopyText = async (text: string): Promise<void> => {
13
+ // Modern browsers
14
+ if (navigator?.clipboard?.writeText) {
15
+ await navigator.clipboard.writeText(text)
16
+ return
17
+ }
18
+
19
+ // Legacy fallback (deprecated but still works)
20
+ const textarea = document.createElement('textarea')
21
+ textarea.value = text
22
+ textarea.style.position = 'fixed'
23
+ textarea.style.opacity = '0'
24
+
25
+ document.body.appendChild(textarea)
26
+ textarea.focus()
27
+ textarea.select()
28
+
29
+ try {
30
+ document.execCommand('copy')
31
+ } finally {
32
+ document.body.removeChild(textarea)
33
+ }
34
+ }
35
+
36
+ const copyText = useCallback(async (text: string) => {
37
+ try {
38
+ await handleCopyText(text)
39
+ setIsCopying(true)
40
+ setTimeout(() => setIsCopying(false), 1000)
41
+ } catch {
42
+ setIsCopying(false)
43
+ }
44
+ }, [])
45
+
46
+ return [isCopying, copyText] as const
47
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ import Layout from '../layouts/layout.astro'
3
+ import MarqyHero from '../components/MarqyHero'
4
+ import MarqySpeedDemo from '../components/MarqySpeedDemo'
5
+ import MarqyVerticalDemo from '../components/MarqyVerticalDemo'
6
+ import InUse from '../components/InUse.astro'
7
+ import QuickStart from '../components/QuickStart.astro'
8
+ import Playground from '../components/Playground.astro'
9
+ ---
10
+
11
+ <Layout>
12
+
13
+ <!-- Nav -->
14
+ <header class="sticky top-0 inset-x-0 w-full z-10 flex items-center justify-between backdrop-blur-sm border-b p-20 bg-black/60">
15
+ <h1 class="flex items-baseline">
16
+ <span class="text-h1 uppercase">marqy</span>
17
+ <span class="text-p0 text-white/50">v_1.0.0</span>
18
+ </h1>
19
+ <nav>
20
+ <ul class="flex items-center gap-10">
21
+ <li>
22
+ <a
23
+ href="https://github.com/ndimatteo/marqy"
24
+ target="_blank"
25
+ rel="noopener noreferrer"
26
+ class="group text-p1"
27
+ >[ <span class="uppercase group-hover:animate-flicker-opacity">GitHub</span> ]</a>
28
+ </li>
29
+ <li>
30
+ <a
31
+ href="https://www.npmjs.com/package/marqy"
32
+ target="_blank"
33
+ rel="noopener noreferrer"
34
+ class="group text-p1"
35
+ >
36
+ [ <span class="uppercase group-hover:animate-flicker-opacity">NPM</span> ]
37
+ </a>
38
+ </li>
39
+ </ul>
40
+ </nav>
41
+ </header>
42
+
43
+ <!-- Hero (React island) -->
44
+ <MarqyHero client:load />
45
+
46
+ <Playground />
47
+
48
+ <MarqyVerticalDemo client:load />
49
+
50
+ <QuickStart />
51
+
52
+ <MarqySpeedDemo client:load />
53
+
54
+ <InUse />
55
+
56
+ </Layout>
@@ -0,0 +1,155 @@
1
+ @layer base {
2
+ html {
3
+ font-size: 10px;
4
+ }
5
+
6
+ html,
7
+ body {
8
+ -webkit-tap-highlight-color: transparent;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ text-rendering: optimizeLegibility;
12
+ }
13
+
14
+ body {
15
+ display: flex;
16
+ flex-direction: column;
17
+ min-height: 100svh;
18
+ font-family: var(--font-sans);
19
+ background-color: var(--color-black);
20
+ color: var(--color-white);
21
+ font-size: calc(var(--spacing) * 10);
22
+ line-height: 1.25;
23
+
24
+ @media (width >= 800px) {
25
+ font-size: calc(var(--spacing) * 12);
26
+ }
27
+
28
+ & *::selection {
29
+ background-color: var(--color-white);
30
+ color: var(--color-black);
31
+ }
32
+
33
+ &::selection {
34
+ background-color: var(--color-white);
35
+ color: var(--color-black);
36
+ }
37
+ }
38
+
39
+ strong {
40
+ font-weight: 700;
41
+ }
42
+
43
+ em {
44
+ font-style: italic;
45
+ }
46
+
47
+ ul,
48
+ ol {
49
+ margin: calc(var(--spacing) * 0);
50
+ padding: calc(var(--spacing) * 0);
51
+ list-style-type: none;
52
+ }
53
+
54
+ /* Links */
55
+ a,
56
+ button {
57
+ color: currentColor;
58
+
59
+ &:focus-visible {
60
+ outline-style: solid;
61
+ outline-width: 2px;
62
+ outline-offset: 3px;
63
+ outline-color: var(--color-white);
64
+ }
65
+ }
66
+
67
+ button {
68
+ appearance: none;
69
+ text-decoration-line: none;
70
+ }
71
+
72
+ /* Images */
73
+ figure {
74
+ margin: calc(var(--spacing) * 0);
75
+ max-width: none;
76
+ }
77
+
78
+ img,
79
+ video {
80
+ max-width: none;
81
+ }
82
+
83
+ /* Misc. */
84
+ hr {
85
+ margin: calc(var(--spacing) * 40) 0;
86
+ border-top-width: 1px;
87
+ color: var(--color-white);
88
+
89
+ @media (width >= 800px) {
90
+ margin: calc(var(--spacing) * 60) 0;
91
+ }
92
+ }
93
+
94
+ input,
95
+ textarea {
96
+ position: relative;
97
+ width: 100%;
98
+ appearance: none;
99
+ border-radius: 0;
100
+ background-color: transparent;
101
+ background-image: none !important;
102
+ text-align: start;
103
+
104
+ &:-webkit-autofill,
105
+ &:-webkit-autofill:hover,
106
+ &:-webkit-autofill:focus,
107
+ &:-webkit-autofill:active {
108
+ background-color: transparent;
109
+ color: currentColor;
110
+ }
111
+ }
112
+
113
+ input[type='search']::-webkit-search-cancel-button {
114
+ -webkit-appearance: none;
115
+ appearance: none;
116
+ display: none;
117
+ }
118
+
119
+ textarea {
120
+ resize: none;
121
+ }
122
+
123
+ /* global focus state */
124
+ a,
125
+ button,
126
+ details {
127
+ }
128
+
129
+ input,
130
+ textarea,
131
+ select {
132
+ }
133
+
134
+ /* Section focus state */
135
+ section {
136
+ &:focus {
137
+ outline-style: none;
138
+ }
139
+ }
140
+
141
+ /* Hide native video UI */
142
+ video::-webkit-media-controls {
143
+ display: none !important;
144
+ -webkit-appearance: none;
145
+ }
146
+
147
+ pre {
148
+ padding: calc(var(--spacing) * 20);
149
+ border-radius: calc(5 * 0.1rem);
150
+ }
151
+
152
+ [data-reveal] {
153
+ opacity: 0;
154
+ }
155
+ }
@@ -0,0 +1,123 @@
1
+ @layer components {
2
+ /* Headline Copy */
3
+
4
+ .text-h1 {
5
+ font-family: var(--font-headline);
6
+ font-size: calc(var(--spacing) * 15);
7
+ font-weight: 700;
8
+ line-height: 1;
9
+
10
+ &:first-child {
11
+ margin-top: 0;
12
+ }
13
+
14
+ &:last-child {
15
+ margin-bottom: 0;
16
+ }
17
+
18
+ @media (width >= 800px) {
19
+ font-size: calc(var(--spacing) * 20);
20
+ }
21
+
22
+ @supports ((text-box-trim: trim-both) and (text-box-edge: cap alphabetic)) {
23
+ text-box-trim: trim-both;
24
+ text-box-edge: cap alphabetic;
25
+ }
26
+ }
27
+
28
+ .text-h3 {
29
+ font-family: var(--font-headline);
30
+ font-size: calc(var(--spacing) * 12);
31
+ font-weight: 700;
32
+ line-height: 1;
33
+
34
+ &:first-child {
35
+ margin-top: 0;
36
+ }
37
+
38
+ &:last-child {
39
+ margin-bottom: 0;
40
+ }
41
+
42
+ @media (width >= 800px) {
43
+ font-size: calc(var(--spacing) * 14);
44
+ }
45
+
46
+ @supports ((text-box-trim: trim-both) and (text-box-edge: cap alphabetic)) {
47
+ text-box-trim: trim-both;
48
+ text-box-edge: cap alphabetic;
49
+ }
50
+ }
51
+
52
+ .text-jumbo {
53
+ font-family: var(--font-headline);
54
+ font-size: calc(var(--spacing) * 15);
55
+ font-weight: 700;
56
+ line-height: 1;
57
+
58
+ &:first-child {
59
+ margin-top: 0;
60
+ }
61
+
62
+ &:last-child {
63
+ margin-bottom: 0;
64
+ }
65
+
66
+ @media (width >= 800px) {
67
+ font-size: calc(var(--spacing) * 20);
68
+ }
69
+
70
+ @supports ((text-box-trim: trim-both) and (text-box-edge: cap alphabetic)) {
71
+ text-box-trim: trim-both;
72
+ text-box-edge: cap alphabetic;
73
+ }
74
+ }
75
+
76
+ .text-p0 {
77
+ font-family: var(--font-mono);
78
+ font-size: calc(var(--spacing) * 8);
79
+ font-weight: 700;
80
+ line-height: 1;
81
+
82
+ &:first-child {
83
+ margin-top: 0;
84
+ }
85
+
86
+ &:last-child {
87
+ margin-bottom: 0;
88
+ }
89
+
90
+ @media (width >= 800px) {
91
+ font-size: calc(var(--spacing) * 10);
92
+ }
93
+
94
+ @supports ((text-box-trim: trim-both) and (text-box-edge: cap alphabetic)) {
95
+ text-box-trim: trim-both;
96
+ text-box-edge: cap alphabetic;
97
+ }
98
+ }
99
+
100
+ .text-p1 {
101
+ font-family: var(--font-mono);
102
+ font-size: calc(var(--spacing) * 10);
103
+ font-weight: 700;
104
+ line-height: 1;
105
+
106
+ &:first-child {
107
+ margin-top: 0;
108
+ }
109
+
110
+ &:last-child {
111
+ margin-bottom: 0;
112
+ }
113
+
114
+ @media (width >= 800px) {
115
+ font-size: calc(var(--spacing) * 15);
116
+ }
117
+
118
+ @supports ((text-box-trim: trim-both) and (text-box-edge: cap alphabetic)) {
119
+ text-box-trim: trim-both;
120
+ text-box-edge: cap alphabetic;
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,23 @@
1
+ @utility grid-stack {
2
+ grid-area: 1/-1;
3
+ }
4
+
5
+ @utility clean-btn {
6
+ appearance: none;
7
+ text-decoration-line: none;
8
+ }
9
+
10
+ /* Scrollbar helpers */
11
+ @utility hide-scrollbar {
12
+ -ms-overflow-style: none;
13
+ scrollbar-width: none;
14
+
15
+ &::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+ }
19
+
20
+ @utility overflow-scroll {
21
+ overflow: auto;
22
+ -webkit-overflow-scrolling: touch;
23
+ }