mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 03:37:36 +00:00
- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
4.2 KiB
4.2 KiB
Creating Mixins
What This Covers
How to author your own reusable host-element behavior with createMixin. Read this when the task involves:
- Combining multiple low-level events or DOM hooks into one semantic mixin
- Dispatching custom DOM events from a host node
- Encapsulating imperative DOM setup that several components share
- Typing custom events on
HTMLElementEventMapfor use withon(...)
For the built-in mixins most code should use, see mixins-styling-events.md.
Use createMixin from remix/ui to author reusable host-element behavior.
Most app code should use built-in core mixins (on, css, ref, link, attrs) and animation mixins from remix/ui/animation. Create custom mixins when combining multiple low-level events into one semantic event, or when the pattern is reused across components.
Core Semantics
- A mixin handle is tied to one mounted host node lifecycle.
insertis the host-node availability point for imperative setup.removeis teardown for that same lifecycle.queueTaskruns post-commit and receives(node, signal)for mixins.- Mixin render functions should stay pure; side effects belong in
insert,remove, or queued work.
import { createMixin } from 'remix/ui'
let myMixin = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
// event.node is the mounted host node
})
handle.addEventListener('remove', () => {
// Clean up listeners, timers, observers
})
return (props) => {
handle.queueTask((node) => {
// Post-commit work that needs the concrete host node
})
return <handle.element {...props} />
}
})
Patterns
Pure prop transform
let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
<handle.element {...props} title={title} />
))
Lifecycle-managed imperative setup
let withFocus = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
event.node.focus()
})
return (props) => <handle.element {...props} />
})
Custom Event Mixins
Create event mixins when you combine multiple low-level events into one semantic custom event that is reused across components.
- Namespace custom event names (
myapp:*) to avoid collisions. - Extend
Eventwith the data consumers need. - Declare the event on
HTMLElementEventMapfor type safety withon(...). - Dispatch from the host node inside the mixin.
import { createMixin, on } from 'remix/ui'
export let dragReleaseType = 'myapp:drag-release' as const
declare global {
interface HTMLElementEventMap {
[dragReleaseType]: DragReleaseEvent
}
}
export class DragReleaseEvent extends Event {
velocityX: number
velocityY: number
constructor(init: { velocityX: number; velocityY: number }) {
super(dragReleaseType, { bubbles: true, cancelable: true })
this.velocityX = init.velocityX
this.velocityY = init.velocityY
}
}
export let dragRelease = createMixin<HTMLElement>((handle) => {
let node: HTMLElement | undefined
let tracking = false
let velocityX = 0
let velocityY = 0
let lastX = 0
let lastY = 0
let lastT = 0
handle.addEventListener('insert', (event) => {
node = event.node
})
return () => (
<handle.element
mix={[
on('pointerdown', (event) => {
if (!event.isPrimary) return
tracking = true
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
node?.setPointerCapture(event.pointerId)
}),
on('pointermove', (event) => {
if (!tracking) return
let dt = Math.max(1, event.timeStamp - lastT)
velocityX = (event.clientX - lastX) / dt
velocityY = (event.clientY - lastY) / dt
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
}),
on('pointerup', () => {
if (!tracking) return
tracking = false
node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
}),
]}
/>
)
})
Consume it:
<div
mix={[
dragRelease(),
on(dragReleaseType, (event) => {
console.log('velocity:', event.velocityX, event.velocityY)
}),
]}
/>