@hina-ui/vue 1.7.13 → 1.7.14

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.
Files changed (68) hide show
  1. package/dist/index.js +1295 -283
  2. package/dist/index.js.map +1 -1
  3. package/dist/shared/lib/thumbhash.ts +12 -0
  4. package/dist/shared/lib/tour-engine.ts +721 -0
  5. package/dist/shared/lib/tour.ts +202 -24
  6. package/dist/shared/locale/en-US.ts +1 -0
  7. package/dist/shared/locale/ja-JP.ts +1 -0
  8. package/dist/shared/locale/types.ts +1 -0
  9. package/dist/shared/locale/zh-CN.ts +1 -0
  10. package/dist/shared/primitives/dialog/index.ts +2 -2
  11. package/dist/shared/primitives/dismissable-layer/index.ts +48 -1
  12. package/dist/shared/primitives/focus-scope/index.ts +5 -3
  13. package/dist/shared/styles/semantic.css +1 -0
  14. package/dist/shared/styles/tour.css +107 -2
  15. package/dist/shared/variants/image.ts +28 -0
  16. package/dist/shared/variants/tour.ts +22 -4
  17. package/dist/types/shared/src/lib/thumbhash.d.ts +2 -0
  18. package/dist/types/shared/src/lib/thumbhash.d.ts.map +1 -0
  19. package/dist/types/shared/src/lib/tour-engine.d.ts +47 -0
  20. package/dist/types/shared/src/lib/tour-engine.d.ts.map +1 -0
  21. package/dist/types/shared/src/lib/tour.d.ts +65 -9
  22. package/dist/types/shared/src/lib/tour.d.ts.map +1 -1
  23. package/dist/types/shared/src/locale/en-US.d.ts.map +1 -1
  24. package/dist/types/shared/src/locale/ja-JP.d.ts.map +1 -1
  25. package/dist/types/shared/src/locale/types.d.ts +1 -0
  26. package/dist/types/shared/src/locale/types.d.ts.map +1 -1
  27. package/dist/types/shared/src/locale/zh-CN.d.ts.map +1 -1
  28. package/dist/types/shared/src/primitives/dialog/index.d.ts +1 -1
  29. package/dist/types/shared/src/primitives/dialog/index.d.ts.map +1 -1
  30. package/dist/types/shared/src/primitives/dismissable-layer/index.d.ts +1 -0
  31. package/dist/types/shared/src/primitives/dismissable-layer/index.d.ts.map +1 -1
  32. package/dist/types/shared/src/primitives/focus-scope/index.d.ts.map +1 -1
  33. package/dist/types/shared/src/variants/image.d.ts +35 -0
  34. package/dist/types/shared/src/variants/image.d.ts.map +1 -1
  35. package/dist/types/shared/src/variants/tour.d.ts +9 -4
  36. package/dist/types/shared/src/variants/tour.d.ts.map +1 -1
  37. package/dist/types/vue/src/components/button/Button.vue.d.ts.map +1 -1
  38. package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts +3 -0
  39. package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts.map +1 -1
  40. package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts +3 -0
  41. package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts.map +1 -1
  42. package/dist/types/vue/src/components/image/Image.vue.d.ts +1 -0
  43. package/dist/types/vue/src/components/image/Image.vue.d.ts.map +1 -1
  44. package/dist/types/vue/src/components/image/composables/useImage.d.ts +2 -0
  45. package/dist/types/vue/src/components/image/composables/useImage.d.ts.map +1 -1
  46. package/dist/types/vue/src/components/overlay-branch/OverlayBranch.vue.d.ts +17 -0
  47. package/dist/types/vue/src/components/overlay-branch/OverlayBranch.vue.d.ts.map +1 -0
  48. package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts +3 -0
  49. package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts.map +1 -1
  50. package/dist/types/vue/src/components/tour/Tour.vue.d.ts +10 -1
  51. package/dist/types/vue/src/components/tour/Tour.vue.d.ts.map +1 -1
  52. package/dist/types/vue/src/components/tour/types.d.ts +9 -3
  53. package/dist/types/vue/src/components/tour/types.d.ts.map +1 -1
  54. package/dist/types/vue/src/index.d.ts +2 -1
  55. package/dist/types/vue/src/index.d.ts.map +1 -1
  56. package/dist/types/vue/src/primitives/dismissable-layer/DismissableLayerBranch.d.ts.map +1 -1
  57. package/package.json +2 -1
  58. package/src/components/button/Button.vue +1 -1
  59. package/src/components/dialog/Dialog.vue +14 -1
  60. package/src/components/drawer/Drawer.vue +14 -1
  61. package/src/components/image/Image.vue +58 -24
  62. package/src/components/image/composables/useImage.ts +46 -21
  63. package/src/components/overlay-branch/OverlayBranch.vue +14 -0
  64. package/src/components/sheet/Sheet.vue +14 -1
  65. package/src/components/tour/Tour.vue +302 -141
  66. package/src/components/tour/types.ts +33 -2
  67. package/src/index.ts +11 -0
  68. package/src/primitives/dismissable-layer/DismissableLayerBranch.ts +9 -18
@@ -1,11 +1,70 @@
1
1
  import { autoUpdate } from '@floating-ui/dom'
2
- import { hideOthers } from 'aria-hidden'
3
- import { isHighestLayer } from '../primitives/dismissable-layer'
2
+ import { hideOutsideLayers, isHighestLayer } from '../primitives/dismissable-layer'
4
3
  import { getActiveElement, getTabbableCandidates } from '../primitives/focus-scope'
4
+ import { cssTimeToMs } from './css-time'
5
5
 
6
6
  export type TourSide = 'top' | 'right' | 'bottom' | 'left'
7
7
  export type TourAlign = 'start' | 'center' | 'end'
8
- export type TourTarget = string | HTMLElement | (() => HTMLElement | null | undefined) | null
8
+
9
+ export interface TourRectLike {
10
+ x?: number
11
+ y?: number
12
+ left?: number
13
+ top?: number
14
+ width: number
15
+ height: number
16
+ }
17
+
18
+ export interface TourVirtualElement {
19
+ getBoundingClientRect: () => TourRectLike
20
+ contextElement?: Element
21
+ }
22
+
23
+ export type TourTargetItem = string | Element | TourRectLike | TourVirtualElement
24
+ export type TourTarget =
25
+ | TourTargetItem
26
+ | TourTargetItem[]
27
+ | (() => TourTargetItem | TourTargetItem[] | null | undefined)
28
+ | null
29
+
30
+ export type TourPointerKind = 'mouse' | 'touch'
31
+ export type TourCloseReason = 'finish' | 'skip' | 'escape' | 'close'
32
+
33
+ export interface TourPoint {
34
+ x: number
35
+ y: number
36
+ }
37
+
38
+ export interface TourPointer {
39
+ move: (target: TourTarget | TourPoint, duration?: number) => Promise<void>
40
+ tap: () => Promise<void>
41
+ press: (duration?: number) => Promise<void>
42
+ drag: (
43
+ target: TourTarget | TourPoint,
44
+ duration?: number,
45
+ onMove?: (point: TourPoint) => void,
46
+ ) => Promise<void>
47
+ key: (key: string, duration?: number) => Promise<void>
48
+ hide: () => Promise<void>
49
+ }
50
+
51
+ export interface TourSpotlightOptions {
52
+ padding?: number
53
+ radius?: number
54
+ wait?: number
55
+ }
56
+
57
+ export interface TourPlayContext {
58
+ step: number
59
+ signal: AbortSignal
60
+ defer: (cleanup: () => void) => void
61
+ pointer: TourPointer
62
+ spotlight: (target?: TourTarget, options?: TourSpotlightOptions) => Promise<void>
63
+ waitFor: (target: TourTarget, timeout?: number) => Promise<boolean>
64
+ sleep: (ms: number) => Promise<void>
65
+ }
66
+
67
+ export type TourPlay = (context: TourPlayContext) => void | Promise<void>
9
68
 
10
69
  export interface TourStepBase {
11
70
  target?: TourTarget
@@ -16,8 +75,11 @@ export interface TourStepBase {
16
75
  padding?: number
17
76
  radius?: number
18
77
  blockTarget?: boolean
78
+ blockPage?: boolean
19
79
  scroll?: boolean
20
80
  beacon?: boolean
81
+ wait?: number
82
+ play?: TourPlay
21
83
  }
22
84
 
23
85
  export type TourBeforeChange = (
@@ -48,12 +110,100 @@ export function tourTargetBlocked(step: TourStepBase, blockTarget: boolean) {
48
110
  return step.blockTarget ?? blockTarget
49
111
  }
50
112
 
51
- export function resolveTourTarget(target: TourTarget | undefined): HTMLElement | null {
113
+ export function tourPageBlocked(step: TourStepBase, blockPage: boolean) {
114
+ return step.blockPage ?? blockPage
115
+ }
116
+
117
+ export interface TourResolvedTarget {
118
+ elements: HTMLElement[]
119
+ virtual: boolean
120
+ rect: () => TourRect
121
+ }
122
+
123
+ function toRect(rect: TourRectLike): TourRect {
124
+ return {
125
+ x: rect.left ?? rect.x ?? 0,
126
+ y: rect.top ?? rect.y ?? 0,
127
+ width: rect.width,
128
+ height: rect.height,
129
+ }
130
+ }
131
+
132
+ export function unionRect(rects: TourRect[]): TourRect {
133
+ const left = Math.min(...rects.map(rect => rect.x))
134
+ const top = Math.min(...rects.map(rect => rect.y))
135
+ const right = Math.max(...rects.map(rect => rect.x + rect.width))
136
+ const bottom = Math.max(...rects.map(rect => rect.y + rect.height))
137
+ return { x: left, y: top, width: right - left, height: bottom - top }
138
+ }
139
+
140
+ type TourItemReader = { element: HTMLElement | null; read: () => TourRect }
141
+
142
+ function readItem(item: TourTargetItem): TourItemReader | null {
143
+ if (typeof item === 'string') {
144
+ if (typeof document === 'undefined') return null
145
+ const element = document.querySelector<HTMLElement>(item)
146
+ return element ? { element, read: () => toRect(element.getBoundingClientRect()) } : null
147
+ }
148
+ if (typeof Element !== 'undefined' && item instanceof Element)
149
+ return { element: item as HTMLElement, read: () => toRect(item.getBoundingClientRect()) }
150
+ if (typeof (item as TourVirtualElement).getBoundingClientRect === 'function') {
151
+ const virtual = item as TourVirtualElement
152
+ return { element: null, read: () => toRect(virtual.getBoundingClientRect()) }
153
+ }
154
+ const rect = toRect(item as TourRectLike)
155
+ return { element: null, read: () => rect }
156
+ }
157
+
158
+ export function resolveTourTarget(target: TourTarget | undefined): TourResolvedTarget | null {
52
159
  if (!target) return null
53
- if (typeof target === 'string')
54
- return typeof document === 'undefined' ? null : document.querySelector<HTMLElement>(target)
55
- if (typeof target === 'function') return target() ?? null
56
- return target
160
+ const value = typeof target === 'function' ? target() : target
161
+ if (!value) return null
162
+ const items = Array.isArray(value) ? value : [value]
163
+ if (!items.length) return null
164
+ const readers: TourItemReader[] = []
165
+ for (const item of items) {
166
+ const reader = item ? readItem(item) : null
167
+ if (!reader) return null
168
+ readers.push(reader)
169
+ }
170
+ return {
171
+ elements: readers.flatMap(reader => (reader.element ? [reader.element] : [])),
172
+ virtual: readers.some(reader => !reader.element),
173
+ rect: () => unionRect(readers.map(reader => reader.read())),
174
+ }
175
+ }
176
+
177
+ export function waitForTourTarget(
178
+ target: TourTarget | undefined,
179
+ timeout: number,
180
+ signal?: AbortSignal,
181
+ ): Promise<TourResolvedTarget | null> {
182
+ const found = resolveTourTarget(target)
183
+ if (found || timeout <= 0 || signal?.aborted) return Promise.resolve(found)
184
+ return new Promise(resolve => {
185
+ const check = () => {
186
+ const resolved = resolveTourTarget(target)
187
+ if (resolved) finish(resolved)
188
+ }
189
+ const observer = new MutationObserver(check)
190
+ const interval = window.setInterval(check, 100)
191
+ const timer = window.setTimeout(() => finish(null), timeout)
192
+ const abort = () => finish(null)
193
+ function finish(value: TourResolvedTarget | null) {
194
+ observer.disconnect()
195
+ window.clearInterval(interval)
196
+ window.clearTimeout(timer)
197
+ signal?.removeEventListener('abort', abort)
198
+ resolve(value)
199
+ }
200
+ observer.observe(document.documentElement, {
201
+ childList: true,
202
+ subtree: true,
203
+ attributes: true,
204
+ })
205
+ signal?.addEventListener('abort', abort)
206
+ })
57
207
  }
58
208
 
59
209
  export function clampTourStep(step: number | undefined, count: number) {
@@ -167,11 +317,17 @@ export async function runTourChange(
167
317
  }
168
318
  }
169
319
 
170
- export function tourReference(element: HTMLElement, padding = TOUR_PADDING) {
320
+ export function tourCardGap(element?: Element | null) {
321
+ return cssTimeToMs(
322
+ getComputedStyle(element ?? document.documentElement).getPropertyValue('--hn-duration-exit'),
323
+ )
324
+ }
325
+
326
+ export function tourReference(read: () => TourRect | null, contextElement?: Element | null) {
171
327
  return {
172
- contextElement: element,
328
+ contextElement: contextElement ?? undefined,
173
329
  getBoundingClientRect() {
174
- const rect = spotlightRect(element.getBoundingClientRect(), padding)
330
+ const rect = read() ?? { x: 0, y: 0, width: 0, height: 0 }
175
331
  return {
176
332
  ...rect,
177
333
  left: rect.x,
@@ -183,23 +339,37 @@ export function tourReference(element: HTMLElement, padding = TOUR_PADDING) {
183
339
  }
184
340
  }
185
341
 
186
- export function watchTourTarget(element: HTMLElement, onChange: () => void) {
342
+ export function watchTourTarget(resolved: TourResolvedTarget, onChange: () => void) {
187
343
  onChange()
188
- return autoUpdate(element, document.documentElement, onChange)
344
+ const stops = resolved.elements.map(element =>
345
+ autoUpdate(element, document.documentElement, onChange),
346
+ )
347
+ if (resolved.virtual)
348
+ stops.push(
349
+ autoUpdate(
350
+ { getBoundingClientRect: () => DOMRect.fromRect(resolved.rect()) },
351
+ document.documentElement,
352
+ onChange,
353
+ { animationFrame: true },
354
+ ),
355
+ )
356
+ return () => {
357
+ for (const stop of stops) stop()
358
+ }
189
359
  }
190
360
 
191
- export function isFullyVisible(element: HTMLElement) {
192
- const rect = element.getBoundingClientRect()
361
+ export function isFullyVisible(rect: TourRect) {
193
362
  return (
194
- rect.top >= 0 &&
195
- rect.left >= 0 &&
196
- rect.bottom <= window.innerHeight &&
197
- rect.right <= window.innerWidth
363
+ rect.y >= 0 &&
364
+ rect.x >= 0 &&
365
+ rect.y + rect.height <= window.innerHeight &&
366
+ rect.x + rect.width <= window.innerWidth
198
367
  )
199
368
  }
200
369
 
201
- export function revealTourTarget(element: HTMLElement) {
202
- if (isFullyVisible(element)) return
370
+ export function revealTourTarget(resolved: TourResolvedTarget) {
371
+ const [element] = resolved.elements
372
+ if (!element || isFullyVisible(resolved.rect())) return
203
373
  const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
204
374
  element.scrollIntoView({
205
375
  block: 'center',
@@ -210,15 +380,18 @@ export function revealTourTarget(element: HTMLElement) {
210
380
 
211
381
  export function hideOutsideTour(elements: (HTMLElement | null | undefined)[]) {
212
382
  const present = elements.filter((element): element is HTMLElement => !!element?.isConnected)
213
- return present.length ? hideOthers(present) : () => {}
383
+ return present.length ? hideOutsideLayers(present) : () => {}
384
+ }
385
+
386
+ export function tourOwnsInput(layer: HTMLElement | null | undefined, scripted: boolean) {
387
+ return scripted || isHighestLayer(layer)
214
388
  }
215
389
 
216
390
  export function trapTourFocus(
217
391
  getContainers: () => (HTMLElement | null | undefined)[],
218
- getLayer: () => HTMLElement | null | undefined,
392
+ enabled: () => boolean,
219
393
  ) {
220
394
  const containers = () => getContainers().filter((node): node is HTMLElement => !!node)
221
- const enabled = () => isHighestLayer(getLayer())
222
395
  const inside = (node: Node | null) => !!node && containers().some(c => c.contains(node))
223
396
 
224
397
  function candidates() {
@@ -259,3 +432,8 @@ export function trapTourFocus(
259
432
  document.removeEventListener('keydown', onKeyDown, true)
260
433
  }
261
434
  }
435
+
436
+ export function listenTourKeys(onKeyDown: (event: KeyboardEvent) => void) {
437
+ window.addEventListener('keydown', onKeyDown, true)
438
+ return () => window.removeEventListener('keydown', onKeyDown, true)
439
+ }
@@ -43,6 +43,7 @@ export const enUS: UiMessages = {
43
43
  next: 'Next',
44
44
  finish: 'Done',
45
45
  skip: 'Skip tour',
46
+ replay: 'Replay',
46
47
  progress: (step, total) => `${step} / ${total}`,
47
48
  beacon: (step, title) => (title ? `Open step ${step}: ${title}` : `Open step ${step}`),
48
49
  },
@@ -43,6 +43,7 @@ export const jaJP: UiMessages = {
43
43
  next: '次へ',
44
44
  finish: '完了',
45
45
  skip: 'ガイドをスキップ',
46
+ replay: 'もう一度再生',
46
47
  progress: (step, total) => `${step} / ${total}`,
47
48
  beacon: (step, title) =>
48
49
  title ? `ステップ ${step} を開く:${title}` : `ステップ ${step} を開く`,
@@ -36,6 +36,7 @@ export interface UiMessages {
36
36
  next: string
37
37
  finish: string
38
38
  skip: string
39
+ replay: string
39
40
  progress: (step: number, total: number) => string
40
41
  beacon: (step: number, title: string) => string
41
42
  }
@@ -38,6 +38,7 @@ export const zhCN: UiMessages = {
38
38
  next: '下一步',
39
39
  finish: '完成',
40
40
  skip: '跳过引导',
41
+ replay: '重新演示',
41
42
  progress: (step, total) => `${step} / ${total}`,
42
43
  beacon: (step, title) => (title ? `查看第 ${step} 步:${title}` : `查看第 ${step} 步`),
43
44
  },
@@ -1,4 +1,4 @@
1
- import { hideOthers } from 'aria-hidden'
1
+ import { hideOutsideLayers } from '../dismissable-layer'
2
2
 
3
3
  export function dialogWarnings({
4
4
  titleName = 'DialogTitle',
@@ -40,7 +40,7 @@ export function hideOutside(element: Element) {
40
40
  try {
41
41
  insideClosedPopover = !!element.closest('[popover]:not(:popover-open)')
42
42
  } catch {}
43
- return insideClosedPopover ? undefined : hideOthers(element)
43
+ return insideClosedPopover ? undefined : hideOutsideLayers([element])
44
44
  }
45
45
 
46
46
  interface OutsideEvent {
@@ -1,3 +1,5 @@
1
+ import { hideOthers } from 'aria-hidden'
2
+
1
3
  export type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>
2
4
  export type FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>
3
5
 
@@ -35,11 +37,30 @@ export function addLayer(element: HTMLElement) {
35
37
  }
36
38
  }
37
39
 
40
+ const branchInline = new Map<HTMLElement, string>()
41
+
42
+ function syncBranchPointerEvents() {
43
+ const list = [...branches]
44
+ for (const branch of list) branch.style.pointerEvents = branchInline.get(branch) ?? ''
45
+ if (blocking.size === 0 || list.length === 0) return
46
+ const body = list[0]!.ownerDocument.body
47
+ const blocked = body.style.pointerEvents
48
+ body.style.pointerEvents = originalBodyPointerEvents ?? ''
49
+ const natural = list.map(branch => getComputedStyle(branch).pointerEvents)
50
+ body.style.pointerEvents = blocked
51
+ list.forEach((branch, index) => (branch.style.pointerEvents = natural[index]!))
52
+ }
53
+
38
54
  export function addBranch(element: HTMLElement) {
55
+ if (!branches.has(element)) branchInline.set(element, element.style.pointerEvents)
39
56
  branches.add(element)
57
+ syncBranchPointerEvents()
40
58
  changed()
41
59
  return () => {
42
- if (branches.delete(element)) changed()
60
+ if (!branches.delete(element)) return
61
+ element.style.pointerEvents = branchInline.get(element) ?? ''
62
+ branchInline.delete(element)
63
+ changed()
43
64
  }
44
65
  }
45
66
 
@@ -50,11 +71,13 @@ export function blockOutsidePointerEvents(element: HTMLElement) {
50
71
  body.style.pointerEvents = 'none'
51
72
  }
52
73
  blocking.add(element)
74
+ syncBranchPointerEvents()
53
75
  changed()
54
76
  return () => {
55
77
  if (!blocking.delete(element)) return
56
78
  if (blocking.size === 0 && originalBodyPointerEvents != null)
57
79
  body.style.pointerEvents = originalBodyPointerEvents
80
+ syncBranchPointerEvents()
58
81
  changed()
59
82
  }
60
83
  }
@@ -62,6 +85,7 @@ export function blockOutsidePointerEvents(element: HTMLElement) {
62
85
  export function releaseLayer(element: HTMLElement) {
63
86
  const removed = layers.delete(element)
64
87
  const unblocked = blocking.delete(element)
88
+ if (unblocked) syncBranchPointerEvents()
65
89
  if (removed || unblocked) changed()
66
90
  }
67
91
 
@@ -82,6 +106,7 @@ export function bodyPointerEventsDisabled() {
82
106
  }
83
107
 
84
108
  export function pointerEventsEnabled(element: HTMLElement | null | undefined) {
109
+ if (inBranch(element ?? null)) return true
85
110
  const highest = [...blocking].at(-1)
86
111
  return layerIndex(element) >= (highest ? [...layers].indexOf(highest) : -1)
87
112
  }
@@ -95,6 +120,28 @@ export function inBranch(target: EventTarget | null) {
95
120
  return [...branches].some(branch => branch.contains(target as Node | null))
96
121
  }
97
122
 
123
+ export function hideOutsideLayers(targets: Element[]) {
124
+ const connected = () => [...branches].filter(branch => branch.isConnected)
125
+ let kept = connected()
126
+ const hide = () => {
127
+ const visible = [...targets.filter(target => target.isConnected), ...kept]
128
+ return visible.length ? hideOthers(visible) : () => {}
129
+ }
130
+ let undo = hide()
131
+ const stop = subscribeLayers(() => {
132
+ const next = connected()
133
+ if (next.length === kept.length && next.every((branch, index) => branch === kept[index])) return
134
+ kept = next
135
+ const previous = undo
136
+ undo = hide()
137
+ previous()
138
+ })
139
+ return () => {
140
+ stop()
141
+ undo()
142
+ }
143
+ }
144
+
98
145
  export function isLayerExist(layer: HTMLElement, target: EventTarget | null) {
99
146
  if (!(target instanceof Element)) return false
100
147
  const targetLayer = target.closest('[data-dismissable-layer]')
@@ -1,3 +1,5 @@
1
+ import { inBranch } from '../dismissable-layer'
2
+
1
3
  export const AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount'
2
4
  export const AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount'
3
5
  const EVENT_OPTIONS = { bubbles: false, cancelable: true }
@@ -118,21 +120,21 @@ export function trapFocus(container: HTMLElement, scope: FocusScopeAPI) {
118
120
  if (scope.paused) return
119
121
  const target = event.target as HTMLElement | null
120
122
  if (container.contains(target)) lastFocused = target
121
- else focus(lastFocused, { select: true })
123
+ else if (!inBranch(target)) focus(lastFocused, { select: true })
122
124
  }
123
125
 
124
126
  const handleFocusOut = (event: FocusEvent) => {
125
127
  if (scope.paused) return
126
128
  const related = event.relatedTarget as HTMLElement | null
127
129
  if (related === null) return
128
- if (!container.contains(related)) focus(lastFocused, { select: true })
130
+ if (!container.contains(related) && !inBranch(related)) focus(lastFocused, { select: true })
129
131
  }
130
132
 
131
133
  const handleMutations = (mutations: MutationRecord[]) => {
132
134
  if (lastFocused === null) return
133
135
  if (!mutations.some(mutation => mutation.removedNodes.length > 0)) return
134
136
  const active = getActiveElement()
135
- if (active && container.contains(active)) return
137
+ if (active && (container.contains(active) || inBranch(active))) return
136
138
  if (!container.contains(lastFocused)) focus(container)
137
139
  }
138
140
 
@@ -88,6 +88,7 @@
88
88
  --hn-bg-subtle: #141414;
89
89
  --hn-bg-inset: var(--color-neutral-800);
90
90
  --hn-bg-scrim: rgb(0 0 0 / 0.6);
91
+ --hn-bg-veil: color-mix(in srgb, var(--hn-surface) 70%, transparent);
91
92
 
92
93
  --hn-fg-default: var(--color-neutral-150);
93
94
  --hn-fg-max: var(--color-neutral-0);
@@ -13,8 +13,42 @@
13
13
  }
14
14
  }
15
15
 
16
+ @keyframes hn-tour-ripple {
17
+ from {
18
+ opacity: 0.6;
19
+ transform: translate(-50%, -50%) scale(0.2);
20
+ }
21
+ to {
22
+ opacity: 0;
23
+ transform: translate(-50%, -50%) scale(1);
24
+ }
25
+ }
26
+
27
+ @keyframes hn-tour-press {
28
+ from {
29
+ opacity: 0.15;
30
+ transform: translate(-50%, -50%) scale(0.4);
31
+ }
32
+ to {
33
+ opacity: 0.45;
34
+ transform: translate(-50%, -50%) scale(1);
35
+ }
36
+ }
37
+
38
+ @keyframes hn-tour-key-press {
39
+ 50% {
40
+ scale: 0.92;
41
+ }
42
+ }
43
+
16
44
  @utility hn-tour-mask {
17
- transition: clip-path var(--hn-duration-base) var(--hn-ease-move);
45
+ &[data-hn-tour-mask] {
46
+ z-index: calc(var(--hn-z-overlay) + 1);
47
+ }
48
+
49
+ &[data-mask='dim'] {
50
+ backdrop-filter: none;
51
+ }
18
52
 
19
53
  &[data-state='open'][data-hn-tour-hole='shown'] {
20
54
  animation:
@@ -24,6 +58,77 @@
24
58
  &[data-state='open'][data-hn-tour-hole='hidden'] {
25
59
  animation:
26
60
  hn-fade-in var(--hn-duration-base) var(--hn-ease-enter),
27
- hn-tour-hole-out var(--hn-duration-exit) var(--hn-ease-exit);
61
+ hn-tour-hole-out var(--hn-duration-base) var(--hn-ease-move);
62
+ }
63
+ }
64
+
65
+ @utility hn-tour-pointer {
66
+ position: fixed;
67
+ left: 0;
68
+ top: 0;
69
+ z-index: calc(var(--hn-z-overlay) + 2);
70
+ pointer-events: none;
71
+ opacity: 0;
72
+ transition: opacity var(--hn-duration-fast) var(--hn-ease-move);
73
+
74
+ &[data-state='shown'] {
75
+ opacity: 1;
76
+ }
77
+
78
+ & [data-hn-tour-pointer-glyph] {
79
+ transition: scale var(--hn-duration-fast) var(--hn-ease-press);
80
+ }
81
+ &[data-kind='mouse'] [data-hn-tour-pointer-glyph] {
82
+ transform-origin: 4px 2px;
83
+ }
84
+ &[data-pressed] [data-hn-tour-pointer-glyph] {
85
+ scale: 0.85;
86
+ }
87
+
88
+ & [data-hn-tour-ripple],
89
+ & [data-hn-tour-pointer-ring] {
90
+ position: absolute;
91
+ left: 0;
92
+ top: 0;
93
+ width: 3rem;
94
+ height: 3rem;
95
+ border-radius: 9999px;
96
+ pointer-events: none;
97
+ }
98
+ & [data-hn-tour-ripple] {
99
+ border: 2px solid var(--hn-accent);
100
+ animation: hn-tour-ripple var(--hn-duration-slow) var(--hn-ease-enter) forwards;
101
+ }
102
+ & [data-hn-tour-pointer-ring] {
103
+ background-color: var(--hn-accent);
104
+ opacity: 0;
105
+ transform: translate(-50%, -50%);
106
+ }
107
+ &[data-pressed='long'] [data-hn-tour-pointer-ring] {
108
+ animation: hn-tour-press var(--hn-tour-press, 800ms) linear forwards;
109
+ }
110
+
111
+ @media (prefers-reduced-motion: reduce) {
112
+ & [data-hn-tour-ripple],
113
+ &[data-pressed='long'] [data-hn-tour-pointer-ring] {
114
+ animation: none;
115
+ }
116
+ }
117
+ }
118
+
119
+ @utility hn-tour-key {
120
+ position: fixed;
121
+ left: 0;
122
+ top: 0;
123
+ z-index: calc(var(--hn-z-overlay) + 2);
124
+ pointer-events: none;
125
+ opacity: 0;
126
+ transition: opacity var(--hn-duration-fast) var(--hn-ease-move);
127
+
128
+ &[data-state='shown'] {
129
+ opacity: 1;
130
+ }
131
+ &[data-state='shown'] > * {
132
+ animation: hn-tour-key-press var(--hn-duration-base) var(--hn-ease-press);
28
133
  }
29
134
  }
@@ -17,6 +17,9 @@ export const image = tv({
17
17
  lazy: {
18
18
  true: 'transition-opacity duration-(--hn-duration-base) ease-enter motion-reduce:transition-none',
19
19
  },
20
+ placeholder: {
21
+ true: '',
22
+ },
20
23
  fit: {
21
24
  cover: 'object-cover',
22
25
  contain: 'object-contain',
@@ -25,6 +28,31 @@ export const image = tv({
25
28
  'scale-down': 'object-scale-down',
26
29
  },
27
30
  },
31
+ compoundVariants: [
32
+ {
33
+ lazy: true,
34
+ placeholder: true,
35
+ class: 'duration-(--hn-duration-slow) ease-move',
36
+ },
37
+ ],
38
+ defaultVariants: { fit: 'cover' },
39
+ })
40
+
41
+ export const imageFrame = tv({
42
+ base: 'relative isolate block size-full',
43
+ })
44
+
45
+ export const imagePlaceholder = tv({
46
+ base: 'bg-center bg-no-repeat',
47
+ variants: {
48
+ fit: {
49
+ cover: 'bg-cover',
50
+ contain: 'bg-contain',
51
+ fill: 'bg-size-[100%_100%]',
52
+ none: 'bg-cover',
53
+ 'scale-down': 'bg-contain',
54
+ },
55
+ },
28
56
  defaultVariants: { fit: 'cover' },
29
57
  })
30
58
 
@@ -1,19 +1,21 @@
1
1
  import { tv } from '../lib/tv'
2
2
 
3
+ const layer = 'z-[calc(var(--hn-z-overlay)+1)]'
4
+
3
5
  export const tourMask = tv({ base: 'hn-scrim hn-tour-mask' })
4
6
 
5
- export const tourBlocker = tv({ base: 'fixed z-(--hn-z-overlay)' })
7
+ export const tourBlocker = tv({ base: `fixed ${layer}` })
6
8
 
7
9
  export const tourCard = tv({
8
- base: 'relative z-(--hn-z-overlay) flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-3 overflow-visible p-4 shadow-lg outline-none',
10
+ base: `relative ${layer} flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-3 overflow-visible p-4 shadow-lg outline-none`,
9
11
  })
10
12
 
11
13
  export const tourCentered = tv({
12
- base: 'pointer-events-none fixed inset-0 z-(--hn-z-overlay) grid place-items-center p-4 [&>*]:pointer-events-auto',
14
+ base: `pointer-events-none fixed inset-0 ${layer} grid place-items-center p-4 [&>*]:pointer-events-auto`,
13
15
  })
14
16
 
15
17
  export const tourBeacon = tv({
16
- base: 'hn-focus-ring fixed z-(--hn-z-overlay) size-4 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full',
18
+ base: `hn-focus-ring fixed ${layer} size-4 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full`,
17
19
  })
18
20
 
19
21
  export const tourBeaconPulse = tv({
@@ -27,3 +29,19 @@ export const tourBeaconDot = tv({
27
29
  export const tourHeader = tv({ base: 'flex items-start gap-2' })
28
30
 
29
31
  export const tourFooter = tv({ base: 'flex items-center gap-2' })
32
+
33
+ export const tourPointer = tv({ base: 'hn-tour-pointer' })
34
+
35
+ export const tourPointerArrow = tv({
36
+ base: 'fill-fg stroke-surface absolute top-0 size-6 -translate-x-1 -translate-y-0.5 drop-shadow-md',
37
+ })
38
+
39
+ export const tourPointerTouch = tv({
40
+ base: 'bg-fg/30 ring-surface absolute top-0 block size-9 -translate-x-1/2 -translate-y-1/2 rounded-full shadow-md ring-2',
41
+ })
42
+
43
+ export const tourKey = tv({ base: 'hn-tour-key' })
44
+
45
+ export const tourKeyCap = tv({
46
+ base: 'border-line bg-surface text-fg block min-w-8 -translate-x-1/2 rounded-md border px-2 py-1 text-center text-sm font-medium whitespace-nowrap shadow-md',
47
+ })
@@ -0,0 +1,2 @@
1
+ export declare function thumbhashToDataUrl(hash: string): string | null;
2
+ //# sourceMappingURL=thumbhash.d.ts.map