mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-04 19:57:37 +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
154 lines
4.2 KiB
Markdown
154 lines
4.2 KiB
Markdown
# 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 `HTMLElementEventMap` for use with `on(...)`
|
|
|
|
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
|
|
|
|
1. A mixin handle is tied to one mounted host node lifecycle.
|
|
2. `insert` is the host-node availability point for imperative setup.
|
|
3. `remove` is teardown for that same lifecycle.
|
|
4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.
|
|
5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued work.
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
|
|
<handle.element {...props} title={title} />
|
|
))
|
|
```
|
|
|
|
### Lifecycle-managed imperative setup
|
|
|
|
```tsx
|
|
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.
|
|
|
|
1. Namespace custom event names (`myapp:*`) to avoid collisions.
|
|
2. Extend `Event` with the data consumers need.
|
|
3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`.
|
|
4. Dispatch from the host node inside the mixin.
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
<div
|
|
mix={[
|
|
dragRelease(),
|
|
on(dragReleaseType, (event) => {
|
|
console.log('velocity:', event.velocityX, event.velocityY)
|
|
}),
|
|
]}
|
|
/>
|
|
```
|