mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-08 07:46:59 +00:00
The generated catalog pages rendered the "Source" accordion title as a
JSX expression, `<Accordion title={`name.html`}>`. Mintlify's search
indexer stringifies expression attribute nodes rather than evaluating
them, so every one of the 399 catalog pages surfaced its Source section
in search results as "[object Object]" (visible on the Scroll Feed page,
among others).
The template literal carried no escaping benefit: every title is a
single-segment `name.html` path, and a double-quoted JSX string handles
it directly. Introduce `mdxStringAttribute`, which emits a plain
`name="value"` attribute and escapes `& " < { }` as HTML character
references (MDX decodes references inside quoted attribute values, and
unlike JSON.stringify it does not rely on backslash escapes, which JSX
strings do not honour). Use it for both the Source accordion title and
the preview iframe title, and regenerate the catalog pages.
Regenerated with `npx tsx scripts/generate-catalog-pages.ts`; the only
change across the 399 pages is the one Accordion title line each.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
353 lines
16 KiB
Plaintext
353 lines
16 KiB
Plaintext
---
|
|
title: "Simulated Cursor"
|
|
description: "A lightweight cursor pointer and click pulse for product walkthroughs and agent UI demos"
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add simulated-cursor" item="simulated-cursor" />
|
|
|
|
That writes one file: `compositions/components/simulated-cursor.html`.
|
|
|
|
<VariablesExplorer
|
|
previewSrc="/public/catalog/components/simulated-cursor.json"
|
|
compositionId="simulated-cursor"
|
|
compositionSrc="compositions/components/simulated-cursor.html"
|
|
variables={[{"id":"size","type":"number","role":"layout","label":"Size","description":"Height and width of the pointer in pixels. The click ring scales with it. Values outside the range clamp to the nearest end and anything that is not a number falls back to 44.","default":44,"min":20,"max":120,"step":2,"unit":"px"},{"id":"tone","type":"enum","role":"style","label":"Tone","description":"Pointer fill and click ring colour: light reads on dark screens, dark reads on light ones, accent rides --hf-ui-accent.","default":"light","options":[{"value":"light","label":"Light"},{"value":"dark","label":"Dark"},{"value":"accent","label":"Accent"}]},{"id":"pulse","type":"enum","role":"style","label":"Click ring","description":"Weight of the ring the click animation expands: subtle is a thin small ring, bold a thick wide one, none removes the ring entirely.","default":"standard","options":[{"value":"subtle","label":"Subtle"},{"value":"standard","label":"Standard"},{"value":"bold","label":"Bold"},{"value":"none","label":"None"}]}]}
|
|
>
|
|
|
|
```html simulated-cursor.html
|
|
<!--
|
|
Simulated Cursor - pointer and click pulse primitive.
|
|
|
|
Paste the markup, CSS and script once, then animate .hf-simulated-cursor
|
|
with x/y values in your paused GSAP timeline. Trigger the click pulse by
|
|
scaling/fading .hf-simulated-cursor-pulse.
|
|
|
|
Variables. The script reads each one, falls back to the declared default on
|
|
anything missing or invalid, and writes the result as a custom property the
|
|
CSS above consumes. The timeline recipe below is untouched by them: it keeps
|
|
driving the pointer position and the ring scale and fade.
|
|
|
|
- size (number 20 to 120, step 2, unit px, default 44): height and width of
|
|
the pointer. The click ring, its offset and its border scale with it, so
|
|
the tip stays anchored at the animated x/y. Values below 20 clamp to 20,
|
|
values above 120 clamp to 120, and anything that is not a number falls
|
|
back to 44.
|
|
- tone (light | dark | accent, default light): pointer fill, outline and
|
|
ring colour. light is the white pointer with a dark outline for dark
|
|
screens, dark flips both for light screens, accent rides --hf-ui-accent.
|
|
- pulse (subtle | standard | bold | none, default standard): weight of the
|
|
click ring. subtle is thin and small, bold thick and wide, none removes
|
|
the ring entirely. The ring stays centred on the same point in every
|
|
case, and it is only on screen while the click animation runs.
|
|
|
|
On every default the result is identical to the original: a 44px white
|
|
pointer with a 22px, 2px white ring offset 9px from the tip.
|
|
-->
|
|
|
|
<div
|
|
class="hf-simulated-cursor"
|
|
aria-hidden="true"
|
|
data-composition-variables='[
|
|
{"id": "size", "type": "number", "role": "layout", "label": "Size", "description": "Height and width of the pointer in pixels. The click ring scales with it. Values outside the range clamp to the nearest end and anything that is not a number falls back to 44.", "default": 44, "min": 20, "max": 120, "step": 2, "unit": "px"},
|
|
{"id": "tone", "type": "enum", "role": "style", "label": "Tone", "description": "Pointer fill and click ring colour: light reads on dark screens, dark reads on light ones, accent rides --hf-ui-accent.", "default": "light", "options": [{"value": "light", "label": "Light"}, {"value": "dark", "label": "Dark"}, {"value": "accent", "label": "Accent"}]},
|
|
{"id": "pulse", "type": "enum", "role": "style", "label": "Click ring", "description": "Weight of the ring the click animation expands: subtle is a thin small ring, bold a thick wide one, none removes the ring entirely.", "default": "standard", "options": [{"value": "subtle", "label": "Subtle"}, {"value": "standard", "label": "Standard"}, {"value": "bold", "label": "Bold"}, {"value": "none", "label": "None"}]}
|
|
]'
|
|
>
|
|
<div class="hf-simulated-cursor-pulse"></div>
|
|
<svg viewBox="0 0 24 24" width="44" height="44">
|
|
<path
|
|
d="M3 2.8 20.6 14 12.8 15.5 9 22 3 2.8Z"
|
|
fill="white"
|
|
stroke="rgba(0,0,0,0.45)"
|
|
stroke-width="1.4"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
<style>
|
|
.hf-simulated-cursor {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: calc(44px * var(--hf-cursor-scale, 1));
|
|
height: calc(44px * var(--hf-cursor-scale, 1));
|
|
transform: translate3d(var(--hf-cursor-x, 0px), var(--hf-cursor-y, 0px), 0);
|
|
pointer-events: none;
|
|
z-index: 999;
|
|
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.24));
|
|
}
|
|
.hf-simulated-cursor svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.hf-simulated-cursor svg path {
|
|
fill: var(--hf-cursor-fill, #ffffff);
|
|
stroke: var(--hf-cursor-edge, rgba(0, 0, 0, 0.45));
|
|
}
|
|
.hf-simulated-cursor-pulse {
|
|
display: var(--hf-cursor-ring-display, block);
|
|
position: absolute;
|
|
left: calc((20px - var(--hf-cursor-ring-size, 22px) / 2) * var(--hf-cursor-scale, 1));
|
|
top: calc((20px - var(--hf-cursor-ring-size, 22px) / 2) * var(--hf-cursor-scale, 1));
|
|
width: calc(var(--hf-cursor-ring-size, 22px) * var(--hf-cursor-scale, 1));
|
|
height: calc(var(--hf-cursor-ring-size, 22px) * var(--hf-cursor-scale, 1));
|
|
border: calc(var(--hf-cursor-ring-width, 2px) * var(--hf-cursor-scale, 1)) solid
|
|
var(--hf-cursor-ring, rgba(255, 255, 255, 0.85));
|
|
border-radius: 999px;
|
|
opacity: 0;
|
|
transform: scale(0.2);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
// Unrecognised or out-of-range overrides return to their declared defaults.
|
|
function pick(table, value, fallback) {
|
|
return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
|
|
}
|
|
|
|
var tones = {
|
|
light: { fill: "#ffffff", edge: "rgba(0, 0, 0, 0.45)", ring: "rgba(255, 255, 255, 0.85)" },
|
|
dark: { fill: "#18181b", edge: "rgba(255, 255, 255, 0.75)", ring: "rgba(24, 24, 27, 0.75)" },
|
|
accent: {
|
|
fill: "var(--hf-ui-accent, #3ce6ac)",
|
|
edge: "rgba(0, 0, 0, 0.45)",
|
|
ring: "var(--hf-ui-accent, #3ce6ac)",
|
|
},
|
|
};
|
|
var pulses = {
|
|
subtle: { size: "18px", width: "1px", display: "block" },
|
|
standard: { size: "22px", width: "2px", display: "block" },
|
|
bold: { size: "28px", width: "3px", display: "block" },
|
|
none: { size: "22px", width: "2px", display: "none" },
|
|
};
|
|
|
|
var size = typeof vars.size === "number" ? vars.size : parseFloat(vars.size);
|
|
if (!isFinite(size)) size = 44;
|
|
size = Math.min(120, Math.max(20, size));
|
|
|
|
var tone = tones[pick(tones, vars.tone, "light")];
|
|
var pulse = pulses[pick(pulses, vars.pulse, "standard")];
|
|
|
|
var roots = document.querySelectorAll(".hf-simulated-cursor");
|
|
for (var i = 0; i < roots.length; i += 1) {
|
|
var root = roots[i];
|
|
root.style.setProperty("--hf-cursor-scale", String(size / 44));
|
|
root.style.setProperty("--hf-cursor-fill", tone.fill);
|
|
root.style.setProperty("--hf-cursor-edge", tone.edge);
|
|
root.style.setProperty("--hf-cursor-ring", tone.ring);
|
|
root.style.setProperty("--hf-cursor-ring-size", pulse.size);
|
|
root.style.setProperty("--hf-cursor-ring-width", pulse.width);
|
|
root.style.setProperty("--hf-cursor-ring-display", pulse.display);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!--
|
|
Timeline integration:
|
|
|
|
tl.to(".hf-simulated-cursor", { "--hf-cursor-x": "760px", "--hf-cursor-y": "420px", duration: 0.8, ease: "power2.inOut" }, startTime);
|
|
tl.fromTo(".hf-simulated-cursor-pulse", { opacity: 0.8, scale: 0.2 }, { opacity: 0, scale: 2.4, duration: 0.45, ease: "power2.out" }, startTime + 0.8);
|
|
-->
|
|
```
|
|
|
|
</VariablesExplorer>
|
|
|
|
## Variables
|
|
|
|
Every one of these has a default, so the piece works untouched. Set the ones you
|
|
want to change on the element:
|
|
|
|
| Variable | Default | Accepts | What it does |
|
|
| --- | --- | --- | --- |
|
|
| `size` | `44` | 20px to 120px, step 2px | Height and width of the pointer in pixels. The click ring scales with it. Values outside the range clamp to the nearest end and anything that is not a number falls back to 44. |
|
|
| `tone` | `light` | `light`, `dark`, `accent` | Pointer fill and click ring colour: light reads on dark screens, dark reads on light ones, accent rides --hf-ui-accent. |
|
|
| `pulse` | `standard` | `subtle`, `standard`, `bold`, `none` | Weight of the ring the click animation expands: subtle is a thin small ring, bold a thick wide one, none removes the ring entirely. |
|
|
|
|
Set them with `data-variable-values` on the element that mounts it. These are the
|
|
defaults, so this behaves exactly like the preview above until you change one:
|
|
|
|
```html wrap
|
|
<div
|
|
data-composition-id="simulated-cursor"
|
|
data-composition-src="compositions/components/simulated-cursor.html"
|
|
data-variable-values='{"size":44,"tone":"light","pulse":"standard"}'
|
|
></div>
|
|
```
|
|
|
|
## Source
|
|
|
|
<Accordion title="simulated-cursor.html">
|
|
|
|
```html
|
|
<!--
|
|
Simulated Cursor - pointer and click pulse primitive.
|
|
|
|
Paste the markup, CSS and script once, then animate .hf-simulated-cursor
|
|
with x/y values in your paused GSAP timeline. Trigger the click pulse by
|
|
scaling/fading .hf-simulated-cursor-pulse.
|
|
|
|
Variables. The script reads each one, falls back to the declared default on
|
|
anything missing or invalid, and writes the result as a custom property the
|
|
CSS above consumes. The timeline recipe below is untouched by them: it keeps
|
|
driving the pointer position and the ring scale and fade.
|
|
|
|
- size (number 20 to 120, step 2, unit px, default 44): height and width of
|
|
the pointer. The click ring, its offset and its border scale with it, so
|
|
the tip stays anchored at the animated x/y. Values below 20 clamp to 20,
|
|
values above 120 clamp to 120, and anything that is not a number falls
|
|
back to 44.
|
|
- tone (light | dark | accent, default light): pointer fill, outline and
|
|
ring colour. light is the white pointer with a dark outline for dark
|
|
screens, dark flips both for light screens, accent rides --hf-ui-accent.
|
|
- pulse (subtle | standard | bold | none, default standard): weight of the
|
|
click ring. subtle is thin and small, bold thick and wide, none removes
|
|
the ring entirely. The ring stays centred on the same point in every
|
|
case, and it is only on screen while the click animation runs.
|
|
|
|
On every default the result is identical to the original: a 44px white
|
|
pointer with a 22px, 2px white ring offset 9px from the tip.
|
|
-->
|
|
|
|
<div
|
|
class="hf-simulated-cursor"
|
|
aria-hidden="true"
|
|
data-composition-variables='[
|
|
{"id": "size", "type": "number", "role": "layout", "label": "Size", "description": "Height and width of the pointer in pixels. The click ring scales with it. Values outside the range clamp to the nearest end and anything that is not a number falls back to 44.", "default": 44, "min": 20, "max": 120, "step": 2, "unit": "px"},
|
|
{"id": "tone", "type": "enum", "role": "style", "label": "Tone", "description": "Pointer fill and click ring colour: light reads on dark screens, dark reads on light ones, accent rides --hf-ui-accent.", "default": "light", "options": [{"value": "light", "label": "Light"}, {"value": "dark", "label": "Dark"}, {"value": "accent", "label": "Accent"}]},
|
|
{"id": "pulse", "type": "enum", "role": "style", "label": "Click ring", "description": "Weight of the ring the click animation expands: subtle is a thin small ring, bold a thick wide one, none removes the ring entirely.", "default": "standard", "options": [{"value": "subtle", "label": "Subtle"}, {"value": "standard", "label": "Standard"}, {"value": "bold", "label": "Bold"}, {"value": "none", "label": "None"}]}
|
|
]'
|
|
>
|
|
<div class="hf-simulated-cursor-pulse"></div>
|
|
<svg viewBox="0 0 24 24" width="44" height="44">
|
|
<path
|
|
d="M3 2.8 20.6 14 12.8 15.5 9 22 3 2.8Z"
|
|
fill="white"
|
|
stroke="rgba(0,0,0,0.45)"
|
|
stroke-width="1.4"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
<style>
|
|
.hf-simulated-cursor {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: calc(44px * var(--hf-cursor-scale, 1));
|
|
height: calc(44px * var(--hf-cursor-scale, 1));
|
|
transform: translate3d(var(--hf-cursor-x, 0px), var(--hf-cursor-y, 0px), 0);
|
|
pointer-events: none;
|
|
z-index: 999;
|
|
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.24));
|
|
}
|
|
.hf-simulated-cursor svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.hf-simulated-cursor svg path {
|
|
fill: var(--hf-cursor-fill, #ffffff);
|
|
stroke: var(--hf-cursor-edge, rgba(0, 0, 0, 0.45));
|
|
}
|
|
.hf-simulated-cursor-pulse {
|
|
display: var(--hf-cursor-ring-display, block);
|
|
position: absolute;
|
|
left: calc((20px - var(--hf-cursor-ring-size, 22px) / 2) * var(--hf-cursor-scale, 1));
|
|
top: calc((20px - var(--hf-cursor-ring-size, 22px) / 2) * var(--hf-cursor-scale, 1));
|
|
width: calc(var(--hf-cursor-ring-size, 22px) * var(--hf-cursor-scale, 1));
|
|
height: calc(var(--hf-cursor-ring-size, 22px) * var(--hf-cursor-scale, 1));
|
|
border: calc(var(--hf-cursor-ring-width, 2px) * var(--hf-cursor-scale, 1)) solid
|
|
var(--hf-cursor-ring, rgba(255, 255, 255, 0.85));
|
|
border-radius: 999px;
|
|
opacity: 0;
|
|
transform: scale(0.2);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
// Unrecognised or out-of-range overrides return to their declared defaults.
|
|
function pick(table, value, fallback) {
|
|
return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
|
|
}
|
|
|
|
var tones = {
|
|
light: { fill: "#ffffff", edge: "rgba(0, 0, 0, 0.45)", ring: "rgba(255, 255, 255, 0.85)" },
|
|
dark: { fill: "#18181b", edge: "rgba(255, 255, 255, 0.75)", ring: "rgba(24, 24, 27, 0.75)" },
|
|
accent: {
|
|
fill: "var(--hf-ui-accent, #3ce6ac)",
|
|
edge: "rgba(0, 0, 0, 0.45)",
|
|
ring: "var(--hf-ui-accent, #3ce6ac)",
|
|
},
|
|
};
|
|
var pulses = {
|
|
subtle: { size: "18px", width: "1px", display: "block" },
|
|
standard: { size: "22px", width: "2px", display: "block" },
|
|
bold: { size: "28px", width: "3px", display: "block" },
|
|
none: { size: "22px", width: "2px", display: "none" },
|
|
};
|
|
|
|
var size = typeof vars.size === "number" ? vars.size : parseFloat(vars.size);
|
|
if (!isFinite(size)) size = 44;
|
|
size = Math.min(120, Math.max(20, size));
|
|
|
|
var tone = tones[pick(tones, vars.tone, "light")];
|
|
var pulse = pulses[pick(pulses, vars.pulse, "standard")];
|
|
|
|
var roots = document.querySelectorAll(".hf-simulated-cursor");
|
|
for (var i = 0; i < roots.length; i += 1) {
|
|
var root = roots[i];
|
|
root.style.setProperty("--hf-cursor-scale", String(size / 44));
|
|
root.style.setProperty("--hf-cursor-fill", tone.fill);
|
|
root.style.setProperty("--hf-cursor-edge", tone.edge);
|
|
root.style.setProperty("--hf-cursor-ring", tone.ring);
|
|
root.style.setProperty("--hf-cursor-ring-size", pulse.size);
|
|
root.style.setProperty("--hf-cursor-ring-width", pulse.width);
|
|
root.style.setProperty("--hf-cursor-ring-display", pulse.display);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!--
|
|
Timeline integration:
|
|
|
|
tl.to(".hf-simulated-cursor", { "--hf-cursor-x": "760px", "--hf-cursor-y": "420px", duration: 0.8, ease: "power2.inOut" }, startTime);
|
|
tl.fromTo(".hf-simulated-cursor-pulse", { opacity: 0.8, scale: 0.2 }, { opacity: 0, scale: 2.4, duration: 0.45, ease: "power2.out" }, startTime + 0.8);
|
|
-->
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
## Usage
|
|
|
|
Open `compositions/components/simulated-cursor.html` and paste its contents into your composition. See the comment header in the file for detailed instructions.
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `video-primitive` `motion-primitive` `cursor` `ui` `walkthrough`.
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|