Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8cc9b72
feat(alpine): create alpine-ts example
niLPotential Nov 19, 2025
7370f62
chore: add example deps
niLPotential Nov 19, 2025
ebe6797
chore: add Head component
niLPotential Nov 19, 2025
421ada1
feat(alpine): add lib files
niLPotential Nov 19, 2025
fb94ac1
feat(alpine): add Nav component
niLPotential Nov 19, 2025
e7c0654
feat(alpine): add style script
niLPotential Nov 19, 2025
79d3900
feat(alpine): add deps
niLPotential Nov 19, 2025
84c59be
chore: add lucide static
niLPotential Nov 19, 2025
c2107f2
feat(alpine): add accordion example
niLPotential Nov 19, 2025
f53cdb9
feat(alpine): add alpine scripts
niLPotential Nov 20, 2025
c10c71f
feat(alpine): add toolbar components
niLPotential Nov 20, 2025
3ca58cc
feat(alpine): finish accordion example
niLPotential Nov 20, 2025
9c138fd
feat(alpine): add angle slider example
niLPotential Nov 20, 2025
067d5a3
feat(alpine): add avatar example
niLPotential Nov 20, 2025
6912c62
feat(alpine): add presence component
niLPotential Nov 20, 2025
1565187
feat(alpine): add checkbox example
niLPotential Nov 20, 2025
8df8ff3
feat(alpine): add switch example
niLPotential Nov 20, 2025
a4887c1
feat(alpine): add editable example
niLPotential Nov 20, 2025
2facb02
feat(alpine): add number input example
niLPotential Nov 21, 2025
bebca40
feat(alpine): add rating group example
niLPotential Nov 21, 2025
7c164cb
feat(alpine): add hover card example
niLPotential Nov 21, 2025
fd6cc89
feat(alpine): add tags input example
niLPotential Nov 21, 2025
225d999
feat(alpine): add clipboard example
niLPotential Nov 21, 2025
264a1ca
feat(alpine): add password input example
niLPotential Nov 21, 2025
9c96a23
fix: import icons as magic
niLPotential Nov 21, 2025
08e99b3
feat(alpine): add pin input example
niLPotential Nov 21, 2025
3bc49cf
feat(alpine): add color picker example
niLPotential Nov 21, 2025
bab0539
feat(alpine): add pagination example
niLPotential Nov 21, 2025
1342317
feat(alpine): add radio group example
niLPotential Nov 21, 2025
59549d7
feat(alpine): add select example
niLPotential Nov 21, 2025
f99a185
feat(alpine): add slider example
niLPotential Nov 21, 2025
dd06fcd
feat(alpine): add tooltip example
niLPotential Nov 21, 2025
5828ea0
feat(alpine): add tabs example
niLPotential Nov 21, 2025
c5841b8
feat(alpine): add toggle example
niLPotential Nov 22, 2025
4c26e1f
feat(alpine): add toggle group example
niLPotential Nov 22, 2025
0f128b9
feat(alpine): add timer examples
niLPotential Nov 22, 2025
409515f
feat(alpine): add steps example
niLPotential Nov 22, 2025
f6a3e21
fix: simplify magics
niLPotential Nov 22, 2025
aabb781
feat(alpine): add scroll area example
niLPotential Nov 22, 2025
c3717d2
feat(alpine): add progress example
niLPotential Nov 22, 2025
18598e6
chore: remove default files
niLPotential Nov 22, 2025
52637fa
fix: add index page
niLPotential Nov 22, 2025
f9382a8
Merge branch 'main' into alpine-ts
niLPotential Nov 22, 2025
0008479
feat(alpine): add dialog examples
niLPotential Nov 22, 2025
49c5e64
feat(alpine): add floating panel example
niLPotential Nov 22, 2025
6d81212
feat(alpine): add controlled checkbox example
niLPotential Nov 22, 2025
5c0ef12
feat(alpine): add range slider example
niLPotential Nov 22, 2025
bf73bbc
fix: handle multiple instances with modifier
niLPotential Nov 23, 2025
e3a6abc
fix: call api magic
niLPotential Nov 23, 2025
d4f5a3a
feat(alpine): add segment control example
niLPotential Nov 23, 2025
37ff543
feat(alpine): add qr code example
niLPotential Nov 23, 2025
040dee1
feat(alpine): add bottom-sheet example
niLPotential Nov 24, 2025
00e8d20
feat(alpine): add carousel examples
niLPotential Nov 25, 2025
37a2925
feat(alpine): add combobox examples
niLPotential Nov 27, 2025
0269696
feat(alpine): add listbox examples
niLPotential Nov 27, 2025
fadadba
feat(alpine): add presence example
niLPotential Nov 28, 2025
af1c9b4
feat(alpine): add file upload example
niLPotential Nov 28, 2025
74a7bd9
feat(alpine): add marquee example
niLPotential Nov 28, 2025
cc9586f
feat(alpine): add popper example
niLPotential Nov 28, 2025
3f56c3f
feat(alpine): add autoresize example
niLPotential Nov 28, 2025
d9dc00f
feat(alpine): add async list example
niLPotential Nov 28, 2025
1f0569c
Merge branch 'main' into alpine-ts
niLPotential Nov 29, 2025
2e65f78
feat(alpine): add menu examples
niLPotential Nov 29, 2025
6610d93
feat(alpine): add context menu examples
niLPotential Nov 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/alpine-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
.data
.nitro
.cache
.output
.env
.env.local
19 changes: 19 additions & 0 deletions examples/alpine-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Nitro starter

Create your full-stack apps and deploy it anywhere with this [Vite](https://vite.dev/) + [Nitro](https://v3.nitro.build/) starter.

## Getting started

```bash
npm install
npm run dev
```

## Deploying

```bash
npm run build
npm run preview
```

Then checkout the [Nitro documentation](https://v3.nitro.build/deploy) to learn more about the different deployment presets.
68 changes: 68 additions & 0 deletions examples/alpine-ts/lib/bindable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Bindable, BindableParams } from "@zag-js/core"
import { isFunction } from "@zag-js/utils"
import Alpine from "alpinejs"

export function bindable<T>(props: () => BindableParams<T>): Bindable<T> {
const initial = props().defaultValue ?? props().value
const eq = props().isEqual ?? Object.is

const v = Alpine.reactive({ value: initial })
const controlled = {
get value() {
return props().value !== undefined
},
}

const valueRef = {
get value() {
return controlled.value ? props().value : v.value
},
}

const setFn = (val: T | ((prev: T) => T)) => {
const prev = controlled.value ? props().value : v.value
const next = isFunction(val) ? val(prev as T) : val

if (props().debug) {
console.log(`[bindable > ${props().debug}] setValue`, { next, prev })
}

if (!controlled.value) v.value = next
if (!eq(next, prev)) {
props().onChange?.(next, prev)
}
}

function get(): T {
return (controlled.value ? props().value : v.value) as T
}

return {
initial,
ref: valueRef,
get,
set(val: T | ((prev: T) => T)) {
setFn(val)
},
invoke(nextValue: T, prevValue: T) {
props().onChange?.(nextValue, prevValue)
},
hash(value: T) {
return props().hash?.(value) ?? String(value)
},
}
}

bindable.cleanup = (fn: VoidFunction) => {
Alpine.onElRemoved(() => fn())
}

bindable.ref = <T>(defaultValue: T) => {
let value = defaultValue
return {
get: () => value,
set: (next: T) => {
value = next
},
}
}
1 change: 1 addition & 0 deletions examples/alpine-ts/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { usePlugin } from "./plugin"
298 changes: 298 additions & 0 deletions examples/alpine-ts/lib/machine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
import type {
ActionsOrFn,
BindableContext,
ChooseFn,
ComputedFn,
EffectsOrFn,
GuardFn,
Machine,
MachineSchema,
Params,
Service,
} from "@zag-js/core"
import { createScope, INIT_STATE, MachineStatus } from "@zag-js/core"
import { compact, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils"
import { bindable } from "./bindable"
import { useRefs } from "./refs"
import { track } from "./track"
import Alpine from "alpinejs"

export function useMachine<T extends MachineSchema>(
machine: Machine<T>,
evaluateProps: (callback: (props: Partial<T["props"]>) => void) => void,
): Service<T> & { init: VoidFunction; destroy: VoidFunction } {
let initialProps = {} as T["props"]
evaluateProps((props) => {
initialProps = props
})
// TODO: cache and update scope
const { id, ids, getRootNode } = initialProps as any
const scope = createScope({ id, ids, getRootNode })

const debug = (...args: any[]) => {
if (machine.debug) console.log(...args)
}

const props = Alpine.reactive({
value: machine.props?.({ props: compact(initialProps), scope }) ?? initialProps,
})
Alpine.effect(() => {
let p = {}
evaluateProps((value) => (p = value))
props.value = machine.props?.({ props: compact(p), scope }) ?? (p as any)
})
const prop = useProp(props)

const context: any = machine.context?.({
prop,
bindable,
scope,
flush,
getContext() {
return ctx
},
getComputed() {
return computed
},
getRefs() {
return refs
},
getEvent() {
return getEvent()
},
})

const ctx: BindableContext<T> = {
get(key) {
return context[key]?.get()
},
set(key, value) {
context[key]?.set(value)
},
initial(key) {
return context[key]?.initial
},
hash(key) {
const current = context[key]?.get()
return context[key]?.hash(current)
},
}

let effects = new Map<string, VoidFunction>()
let transitionRef: any = null

let previousEventRef: { current: any } = { current: null }
let eventRef: { current: any } = { current: { type: "" } }

const getEvent = () => ({
...eventRef.current,
current() {
return eventRef.current
},
previous() {
return previousEventRef.current
},
})

const getState = () => ({
...state,
matches(...values: T["state"][]) {
const currentState = state.get()
return values.includes(currentState)
},
hasTag(tag: T["tag"]) {
const currentState = state.get()
return !!machine.states[currentState]?.tags?.includes(tag)
},
})

const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {})

const getParams = (): Params<T> => ({
state: getState(),
context: ctx,
event: getEvent(),
prop,
send,
action,
guard,
track,
refs,
computed,
flush,
scope,
choose,
})

const action = (keys: ActionsOrFn<T> | undefined) => {
const strs = isFunction(keys) ? keys(getParams()) : keys
if (!strs) return
const fns = strs.map((s) => {
const fn = machine.implementations?.actions?.[s]
if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`)
return fn
})
for (const fn of fns) {
fn?.(getParams())
}
}
const guard = (str: T["guard"] | GuardFn<T>) => {
if (isFunction(str)) return str(getParams())
return machine.implementations?.guards?.[str](getParams())
}

const effect = (keys: EffectsOrFn<T> | undefined) => {
const strs = isFunction(keys) ? keys(getParams()) : keys
if (!strs) return
const fns = strs.map((s) => {
const fn = machine.implementations?.effects?.[s]
if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`)
return fn
})
const cleanups: VoidFunction[] = []
for (const fn of fns) {
const cleanup = fn?.(getParams())
if (cleanup) cleanups.push(cleanup)
}
return () => cleanups.forEach((fn) => fn?.())
}

const choose: ChooseFn<T> = (transitions) => {
return toArray(transitions).find((t) => {
let result = !t.guard
if (isString(t.guard)) result = !!guard(t.guard)
else if (isFunction(t.guard)) result = t.guard(getParams())
return result
})
}

const computed: ComputedFn<T> = (key) => {
ensure(machine.computed, () => `[zag-js] No computed object found on machine`)
const fn = machine.computed[key]
return fn({
context: ctx,
event: getEvent(),
prop,
refs,
scope,
computed: computed as any,
})
}

const state = bindable(() => ({
defaultValue: machine.initialState({ prop }),
onChange(nextState, prevState) {
// compute effects: exit -> transition -> enter

queueMicrotask(() => {
// exit effects
if (prevState) {
const exitEffects = effects.get(prevState)
exitEffects?.()
effects.delete(prevState)
}

// exit actions
if (prevState) {
action(machine.states[prevState]?.exit)
}

// transition actions
action(transitionRef?.actions)

// enter effect
const cleanup = effect(machine.states[nextState]?.effects)
if (cleanup) effects.set(nextState as string, cleanup)

// root entry actions
if (prevState === INIT_STATE) {
action(machine.entry)
const cleanup = effect(machine.effects)
if (cleanup) effects.set(INIT_STATE, cleanup)
}

// enter actions
action(machine.states[nextState]?.entry)
})
},
}))

let status = MachineStatus.NotStarted

const init = () => {
const started = status === MachineStatus.Started
status = MachineStatus.Started
debug(started ? "rehydrating..." : "initializing...")
state.invoke(state.initial!, INIT_STATE)
}

const destroy = () => {
debug("unmounting...")
status = MachineStatus.Stopped

effects.forEach((fn) => fn?.())
effects = new Map()
transitionRef.current = null

action(machine.exit)
}

const send = (event: any) => {
if (status !== MachineStatus.Started) return

previousEventRef.current = eventRef.current
eventRef.current = event

let currentState = state.get()

// @ts-ignore
const transitions = machine.states[currentState].on?.[event.type] ?? machine.on?.[event.type]

const transition = choose(transitions)
if (!transition) return

// save current transition
transitionRef = transition
const target = transition.target ?? currentState

debug("transition", event.type, transition.target || currentState, `(${transition.actions})`)

const changed = target !== currentState
if (changed) {
// state change is high priority
state.set(target)
} else if (transition.reenter && !changed) {
// reenter will re-invoke the current state
state.invoke(currentState, currentState)
} else {
// call transition actions
action(transition.actions)
}
}

machine.watch?.(getParams())

return {
state: getState(),
send,
context: ctx,
prop,
scope,
refs,
computed,
event: getEvent(),
getStatus: () => status,
init,
destroy,
}
}

function useProp<T>(ref: { value: T }) {
return function get<K extends keyof T>(key: K): T[K] {
return ref.value[key]
}
}

function flush(fn: VoidFunction) {
queueMicrotask(() => fn())
}
Loading