@joewinke/jatui 0.4.0 → 0.5.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/CLAUDE.md CHANGED
@@ -68,6 +68,7 @@ Then import:
68
68
  | `ContextMenu` | Floating right-click (or programmatic) action menu, positioned via `use:floating` at a cursor point. Real `<button>`s, per-item `icon`/`variant`/`disabled`, dividers, click-outside + Escape dismiss, arrow-key/Home/End roving nav (focus starts on the first item). Items may declare `submit: SubmitConfig` to POST to a SvelteKit form action instead of (or in addition to) `onSelect` — a hidden `<form>` is built, requestSubmit()ed, then removed, so `use:enhance` fires exactly like a regular form submission. **Items also support nested `submenu: ContextMenuItem[]`** (right-side popover on hover/focus/ArrowRight, chevron indicator, ArrowLeft/Escape closes just the submenu — submenus open on parent-item focus **without stealing focus into the submenu**; ArrowRight is the explicit "enter the submenu" gesture) **and inline `content: Snippet`** (replaces the default icon+label row with an arbitrary widget — the wrapping button still closes the menu, so widgets that shouldn't must `event.stopPropagation()`). `autoFocus` (default true) can be set false to suppress auto-focus of the first item on open. | `open` (bindable), `x`, `y`, `items: ContextMenuItem[]`, `header?`, `zIndex?`, `autoFocus?`, `onClose?` |
69
69
  | `ContextMenuTrigger` | Wraps content with the standard right-click + "⋯" touch affordance, backed by `ContextMenu`. Use instead of hand-rolling `oncontextmenu` + a kebab button per row/card. | `items: ContextMenuItem[]`, `children` (snippet), `header?`, `triggerLabel?`, `hideButton?`, `zIndex?`, `class?` |
70
70
  | `Tooltip` | Component-shaped wrapper around the shared `use:tooltip` action — for cases where you can't easily add an attribute to the target element (wrap a chip / badge / icon in the tooltip instead). Warm/cool window + `aria-describedby` are shared with the action. Prefer `use:tooltip` directly when you own the element. | `content: string`, `placement?`, `delayMs?`, `disabled?`, `display?`, `children` (snippet) |
71
+ | `PresenceStack` | "Who's here" avatar stack for live-collaboration UI — max `maxVisible` avatars + "+N more" overflow bubble, tooltip per avatar ("X is editing FIELD" / "X is viewing"). Purely presentational — feed it viewers from any presence source (e.g. an SSE-backed presence composable). | `viewers: PresenceViewer[]`, `maxVisible?` (default 5), `size?`, `class?` |
71
72
  | `DirtyFormHost` | Composable form dirty-state manager — renders your form body plus a floating "Unsaved changes" pill + Cancel button; wires `beforeunload` and (optional) SvelteKit `beforeNavigate`. Callers who want bespoke pill/Cancel chrome use `createDirtyGuard()` directly and skip the component. Extracted from the F12 pattern in `/admin/roles/[id]`. | `baseline: T`, `current: T`, `onCancel?`, `beforeNavigate?`, `discardPrompt?`, `equals?`, `hideChrome?`, `children` (snippet with `{dirty, cancel}`) |
72
73
  | `BulkActionBar` | Floating action bar that appears when a `listNav` selection is non-empty. Simpler API than `FloatingActionBar`: takes a `Set`, `Array`, or plain number for `selection`; a flat `actions[]` with `{label, onSelect, destructive?}`; and now `groups[]` for dropdown-menu actions (each group is one button on the bar that opens a menu of `{label, onSelect}` items — same shape as `BulkAction`). Flat + grouped can be mixed. Reach for `FloatingActionBar` directly only when you need finer per-item dropdown styling (dot colors, icon snippets). | `selection: Set \| Array \| number`, `actions?: BulkAction[]`, `groups?: BulkActionGroup[]`, `onClear?`, `loading?`, `countLabel?` |
73
74
 
@@ -297,6 +298,10 @@ import type {
297
298
  // ImageVariantSpec — { name, width, height, fit?, background? }
298
299
  // VariantResult — { name, url, width, height, backgroundRemovalFailed? }
299
300
  // VariantState — union: idle | loading | done (VariantResult) | error (message)
301
+
302
+ // PresenceStack types
303
+ import type { PresenceViewer } from "jatui"
304
+ // PresenceViewer — { user_id, name, avatar_url?, typing_scope? }
300
305
  ```
301
306
 
302
307
  ## Live Demos
@@ -0,0 +1,64 @@
1
+ <!--
2
+ PresenceStack — "who's here" avatar stack for live-collaboration UI
3
+ (mos-hwduf.4.4). Purely presentational: feed it a PresenceViewer[] from
4
+ any presence source (e.g. a host app's SSE-backed presence composable).
5
+
6
+ Shows up to maxVisible avatars in input order with a "+N more" overflow
7
+ bubble. Each avatar carries a tooltip: "{name} is editing {typing_scope}"
8
+ when typing_scope is set, else "{name} is viewing".
9
+ -->
10
+ <script lang="ts">
11
+ import type { PresenceViewer } from '../types/presence';
12
+ import Avatar from './messaging/Avatar.svelte';
13
+ import Tooltip from './Tooltip.svelte';
14
+
15
+ interface Props {
16
+ viewers: PresenceViewer[];
17
+ maxVisible?: number;
18
+ size?: 'xs' | 'small' | 'medium' | 'large';
19
+ class?: string;
20
+ }
21
+
22
+ let { viewers = [], maxVisible = 5, size = 'small', class: className = '' }: Props = $props();
23
+
24
+ const visible = $derived(viewers.slice(0, maxVisible));
25
+ const overflow = $derived(viewers.slice(maxVisible));
26
+
27
+ function tipFor(viewer: PresenceViewer): string {
28
+ const who = viewer.name ?? 'Someone';
29
+ return viewer.typing_scope ? `${who} is editing ${viewer.typing_scope}` : `${who} is viewing`;
30
+ }
31
+
32
+ const overflowTip = $derived(overflow.map((v) => v.name ?? 'Someone').join(', '));
33
+
34
+ const overflowSizeClasses: Record<string, string> = {
35
+ xs: 'w-5 h-5 text-[8px]',
36
+ small: 'w-8 h-8 text-xs',
37
+ medium: 'w-10 h-10 text-sm',
38
+ large: 'w-12 h-12 text-base'
39
+ };
40
+ </script>
41
+
42
+ <div class="flex -space-x-2 {className}">
43
+ {#each visible as viewer (viewer.user_id)}
44
+ <Tooltip content={tipFor(viewer)} display="inline-block">
45
+ <div class="border-2 border-base-100 rounded-full">
46
+ <Avatar name={viewer.name ?? '?'} avatarUrl={viewer.avatar_url} {size} />
47
+ </div>
48
+ </Tooltip>
49
+ {/each}
50
+
51
+ {#if overflow.length > 0}
52
+ <Tooltip content={overflowTip} display="inline-block">
53
+ <div class="border-2 border-base-100 rounded-full">
54
+ <div
55
+ class="bg-neutral text-neutral-content rounded-full flex items-center justify-center {overflowSizeClasses[
56
+ size
57
+ ]}"
58
+ >
59
+ <span class="font-bold">+{overflow.length}</span>
60
+ </div>
61
+ </div>
62
+ </Tooltip>
63
+ {/if}
64
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { PresenceViewer } from '../types/presence';
2
+ interface Props {
3
+ viewers: PresenceViewer[];
4
+ maxVisible?: number;
5
+ size?: 'xs' | 'small' | 'medium' | 'large';
6
+ class?: string;
7
+ }
8
+ declare const PresenceStack: import("svelte").Component<Props, {}, "">;
9
+ type PresenceStack = ReturnType<typeof PresenceStack>;
10
+ export default PresenceStack;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ export { default as BulkActionBar } from './components/BulkActionBar.svelte';
41
41
  export type { BulkAction, BulkActionGroup } from './components/BulkActionBar.svelte';
42
42
  export { default as HunkDiffView } from './components/HunkDiffView.svelte';
43
43
  export { default as Tooltip } from './components/Tooltip.svelte';
44
+ export { default as PresenceStack } from './components/PresenceStack.svelte';
45
+ export type { PresenceViewer } from './types/presence';
44
46
  export { default as DirtyFormHost } from './components/DirtyFormHost.svelte';
45
47
  export { createDirtyGuard } from './utils/dirtyGuard';
46
48
  export type { DirtyGuard, DirtyGuardOptions, BeforeNavigateFn } from './utils/dirtyGuard';
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ export { default as BulkActionBar } from './components/BulkActionBar.svelte';
45
45
  export { default as HunkDiffView } from './components/HunkDiffView.svelte';
46
46
  // Primitives — accessibility + form-editor scaffolding
47
47
  export { default as Tooltip } from './components/Tooltip.svelte';
48
+ export { default as PresenceStack } from './components/PresenceStack.svelte';
48
49
  export { default as DirtyFormHost } from './components/DirtyFormHost.svelte';
49
50
  export { createDirtyGuard } from './utils/dirtyGuard';
50
51
  export { default as MediaLightbox } from './components/MediaLightbox.svelte';
@@ -0,0 +1,8 @@
1
+ /** One user currently viewing (and optionally editing a field of) an entity. */
2
+ export interface PresenceViewer {
3
+ user_id: string;
4
+ name: string | null;
5
+ avatar_url?: string | null;
6
+ /** Field/scope the user is actively editing, or null/absent if just viewing. */
7
+ typing_scope?: string | null;
8
+ }
@@ -0,0 +1,3 @@
1
+ // Live-collaboration presence types — framework-agnostic wire shape shared by
2
+ // any "who's viewing/editing this" feature (e.g. an SSE `presence.updated` event).
3
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",