@doan-labs/peek 1.1.0 → 2.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.
package/src/tables.ts CHANGED
@@ -175,6 +175,53 @@ export const COLORS = {
175
175
  aqua: { body: '#BDE1E5', deep: '#8FC7D1' },
176
176
  } as const
177
177
 
178
+ /*
179
+ * Variants: how a face is drawn, never who it is. Every variant reads the
180
+ * same identity, so a name keeps its face, color and parts in all of them.
181
+ */
182
+ export const VARIANTS = ['peek', 'haze', 'bloc'] as const
183
+ export type Variant = (typeof VARIANTS)[number]
184
+
185
+ /**
186
+ * Haze, transcribed from the Haze sheet (01.3). Each ink has a glow and a
187
+ * tint; a body is hazed by the tint of one of its two neighbors on the hue
188
+ * wheel, so the tint is never picked by hand.
189
+ */
190
+ export const HAZE = {
191
+ lavender: { tint: '#C8B1F5', glow: '#F2EDFD' },
192
+ fog: { tint: '#9EC3F2', glow: '#E6F1FF' },
193
+ clay: { tint: '#F4AE93', glow: '#FEECE5' },
194
+ mint: { tint: '#A3DAB5', glow: '#E5F5EA' },
195
+ butter: { tint: '#E1CC7A', glow: '#F4F0E0' },
196
+ rose: { tint: '#F0A9C4', glow: '#FDEAF1' },
197
+ aqua: { tint: '#85D3DC', glow: '#E0F5F7' },
198
+ } as const satisfies Record<keyof typeof COLORS, unknown>
199
+ /** The hue wheel, warm to cool: a body's warm side is the entry before it. */
200
+ export const WHEEL = [
201
+ 'clay',
202
+ 'butter',
203
+ 'mint',
204
+ 'aqua',
205
+ 'fog',
206
+ 'lavender',
207
+ 'rose',
208
+ ] as const satisfies readonly (keyof typeof COLORS)[]
209
+
210
+ /**
211
+ * Bloc's strong inks, palette P2 of the Bloc sheet (02.4): the same hue at
212
+ * OKLCH L 0.72 and C 0.12, butter, mint and aqua held lighter so none goes
213
+ * olive. The strong ink fills the dark half and the trait.
214
+ */
215
+ export const BLOC = {
216
+ lavender: '#B093E5',
217
+ fog: '#7BA8DF',
218
+ clay: '#E38A66',
219
+ mint: '#82CB9B',
220
+ butter: '#E2CA6A',
221
+ rose: '#DF84A8',
222
+ aqua: '#5EC3CD',
223
+ } as const satisfies Record<keyof typeof COLORS, string>
224
+
178
225
  /** Discrete anatomy. Each part reads every channel its feature owns. */
179
226
  export const PARTS = {
180
227
  eyes: ['oval', 'bead', 'ring'],
package/src/vue.ts ADDED
@@ -0,0 +1,133 @@
1
+ /*
2
+ * <Peek name="Ada" /> for Vue 3.5+: the same tree and the same rules as the
3
+ * React component in peek.tsx. ids come from Vue's useId, so the server and
4
+ * the client agree; `class` and `style` fall through to the <svg>.
5
+ *
6
+ * With `animate`, the tree Vue renders stays in the pose the component
7
+ * mounted with and the shared loop writes every frame straight to the DOM,
8
+ * so a change of expression or gaze never reaches Vue's patch.
9
+ */
10
+
11
+ import {
12
+ computed,
13
+ defineComponent,
14
+ h,
15
+ onBeforeUnmount,
16
+ onMounted,
17
+ type PropType,
18
+ ref,
19
+ useId,
20
+ type VNode,
21
+ type VNodeRef,
22
+ watch,
23
+ } from 'vue'
24
+ import { Live } from './animate'
25
+ import { draw, type Gaze, type Node, restPose } from './draw'
26
+ import { type PeekOptions, settle } from './svg'
27
+
28
+ export * from './core'
29
+
30
+ export type PeekProps = Omit<PeekOptions, 'gaze' | 'id'> & {
31
+ name: string
32
+ /** x and y in -1..1, or 'pointer' to follow the pointer (animate only). */
33
+ gaze?: Gaze | 'pointer'
34
+ /** Bring it to life after mount. Off under prefers-reduced-motion. */
35
+ animate?: boolean
36
+ }
37
+
38
+ function toVue(node: Node, ref?: VNodeRef): VNode {
39
+ return h(
40
+ node.tag,
41
+ { key: node.key, ...node.attrs, ref },
42
+ node.children.map((c) => toVue(c)),
43
+ )
44
+ }
45
+
46
+ // Every prop unset stays undefined so the name decides it, as in React.
47
+ // Booleans need their type, or `<Peek animate />` would arrive as ''.
48
+ const any = { type: null, default: undefined }
49
+ const flag = { type: Boolean, default: undefined }
50
+
51
+ export const Peek = defineComponent(
52
+ (props: PeekProps) => {
53
+ const id = `peek${useId().replace(/[^\w-]/g, '')}`
54
+ const svg = ref<SVGSVGElement>()
55
+ let live: Live | null = null
56
+ const fixedOf = (g: PeekProps['gaze']) =>
57
+ Array.isArray(g) ? (g as Gaze) : undefined
58
+ const first = {
59
+ expression: props.expression ?? 'normal',
60
+ gaze: fixedOf(props.gaze),
61
+ }
62
+
63
+ const view = computed(() => {
64
+ const { name, gaze, animate, ...rest } = props
65
+ const { who, pose, opts } = settle(
66
+ name,
67
+ { ...rest, gaze: fixedOf(gaze), id },
68
+ animate,
69
+ )
70
+ const tree = draw(
71
+ who,
72
+ animate ? restPose(who, first.expression, first.gaze) : pose,
73
+ opts,
74
+ )
75
+ // live: the tree only changes when what the face is changes
76
+ return { who, opts, tree, sig: JSON.stringify([who, opts]) }
77
+ })
78
+ const expression = () => props.expression ?? 'normal'
79
+
80
+ onMounted(() => {
81
+ watch(
82
+ [() => props.animate, () => view.value.sig],
83
+ () => {
84
+ live?.destroy()
85
+ live = null
86
+ if (!props.animate || !svg.value) return
87
+ const { tree, who, opts } = view.value
88
+ live = new Live(svg.value, tree, who, opts, expression(), props.gaze)
89
+ },
90
+ { immediate: true, flush: 'post' },
91
+ )
92
+ watch(expression, (e) => live?.setExpression(e), { flush: 'post' })
93
+ watch(
94
+ () => String(props.gaze ?? ''),
95
+ () => live?.setGaze(props.gaze),
96
+ { flush: 'post' },
97
+ )
98
+ })
99
+ onBeforeUnmount(() => live?.destroy())
100
+
101
+ return () => toVue(view.value.tree, svg)
102
+ },
103
+ {
104
+ // biome-ignore lint/style/useVueMultiWordComponentNames: <Peek> in every framework
105
+ name: 'Peek',
106
+ props: {
107
+ name: { type: String, required: true },
108
+ gaze: any,
109
+ animate: { type: Boolean, default: false },
110
+ size: { type: Number, default: undefined },
111
+ expression: any,
112
+ frame: any,
113
+ square: flag,
114
+ title: {
115
+ type: [String, Boolean] as PropType<string | false>,
116
+ default: undefined,
117
+ },
118
+ riso: flag,
119
+ version: { type: Number, default: undefined },
120
+ variant: any,
121
+ face: any,
122
+ color: any,
123
+ eyes: any,
124
+ brows: any,
125
+ mouth: any,
126
+ cheeks: any,
127
+ trait: any,
128
+ eyewear: any,
129
+ headwear: any,
130
+ neckwear: any,
131
+ },
132
+ },
133
+ )