@joewinke/jatui 0.1.27 → 0.1.29

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 (37) hide show
  1. package/CLAUDE.md +3 -3
  2. package/LICENSE +21 -0
  3. package/dist/actions/collapse.d.ts +1 -1
  4. package/dist/actions/collapse.js +16 -30
  5. package/dist/actions/listNav.d.ts +134 -0
  6. package/dist/actions/listNav.js +643 -0
  7. package/dist/actions/railNav.js +78 -89
  8. package/dist/components/InlineEdit.svelte +26 -1
  9. package/dist/components/InlineEdit.svelte.d.ts +13 -0
  10. package/dist/components/PhoneInput.svelte +70 -20
  11. package/dist/components/PhoneInput.svelte.d.ts +5 -2
  12. package/dist/components/ResizableDivider.svelte +148 -18
  13. package/dist/components/ResizableDivider.svelte.d.ts +14 -3
  14. package/dist/components/SignaturePad.svelte +13 -6
  15. package/dist/components/SignaturePad.svelte.d.ts +1 -0
  16. package/dist/components/ThemeSelector.svelte +61 -2
  17. package/dist/components/ThemeSelector.svelte.d.ts +4 -0
  18. package/dist/components/markup.js +59 -62
  19. package/dist/components/session-nav/transcriptModel.js +49 -66
  20. package/dist/data/emojis.js +4 -4
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +1 -0
  23. package/dist/stores/confirmDialog.svelte.js +5 -5
  24. package/dist/stores/inputDialog.svelte.js +5 -5
  25. package/dist/types/booking.js +4 -4
  26. package/dist/types/comments.js +2 -2
  27. package/dist/types/messaging.js +3 -3
  28. package/dist/types/pipeline.js +2 -2
  29. package/dist/utils/currency.js +2 -4
  30. package/dist/utils/daisyuiColors.js +25 -27
  31. package/dist/utils/dateFormatters.js +44 -44
  32. package/dist/utils/googleMapsLoader.js +46 -100
  33. package/dist/utils/mentionParser.js +30 -30
  34. package/dist/utils/phone.js +21 -27
  35. package/dist/utils/phoneFormat.js +9 -9
  36. package/dist/utils/taskUtils.js +1 -1
  37. package/package.json +11 -5
package/CLAUDE.md CHANGED
@@ -24,9 +24,9 @@ Then import:
24
24
  | `TextInput` | Text input with label, error, icon slots | `label`, `value` (bindable), `placeholder`, `error`, `disabled` |
25
25
  | `TextArea` | Multiline text input | `label`, `value` (bindable), `placeholder`, `rows`, `error`, `maxlength` |
26
26
  | `SelectInput` | Dropdown select | `label`, `value` (bindable), `options: SelectOption[]`, `placeholder` |
27
- | `PhoneInput` | Intl phone (country dropdown + tel input, outputs E.164) | `value` (bindable), `name`, `defaultCountry`, `required` |
27
+ | `PhoneInput` | Intl phone (country dropdown + tel input, outputs E.164; caps national digits per country). Fires `onChange(value)` on every change. | `value` (bindable), `name`, `defaultCountry`, `required`, `onChange` |
28
28
  | `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
29
- | `SearchDropdown` | Searchable dropdown with grouped results | `groups: SearchDropdownGroup[]`, `placeholder`, `onSelect` |
29
+ | `SearchDropdown` | Searchable dropdown with grouped results, keyboard nav, avatar/initials options. `appendToBody` escapes table/overflow stacking contexts; `variant='chip'` renders a compact pill; `size='sm'` a smaller trigger; `dropup` opens upward; `clearable` shows an × when a value is set. Exposes `openDropdown()`. | `groups: SearchDropdownGroup[]`, `value`, `onChange`, `placeholder`, `variant`, `size`, `dropup`, `appendToBody`, `clearable`, `colorFn` |
30
30
  | `FilterDropdown` | Multi-select filter dropdown | `label`, `options: string[]`, `selected: Set<string>` (bindable) |
31
31
  | `SortDropdown` | Sort by column with direction | `options: SortOption[]`, `sortBy` (bindable), `sortDir` (bindable) |
32
32
  | `InlineEdit` | Click-to-edit text | `value` (bindable), `segments: DisplaySegment[]`, `onSave` |
@@ -37,7 +37,7 @@ Then import:
37
37
  | `SmartImageEditor` | AI-powered image upload with background removal and multi-variant processing pipeline. **Preferred over `ImageUpload` in editor contexts.** Two modes: simple (drop-in replacement, outputs blob URL) and editor (POST multipart to backend → variant grid). | `value` (bindable), `spec?: ImageProcessingSpec`, `uploadEndpoint?`, `onComplete?`, `accept?`, `maxSizeMb?` |
38
38
  | `FileDropzone` | Multi-file/folder/ZIP drag-drop upload zone. Handles FileSystem API folder recursion, MIME/size validation, and shows a pending-file grid. Calls `onFilesSelected(files, errors)` with validated `File[]`. Accepts already-uploaded files via `uploadedFiles: FileDropzoneFile[]` for post-upload display. | `accept?: string[]`, `maxSize?`, `maxFiles?`, `multiple?`, `uploadedFiles?`, `isUploading?`, `isExtracting?`, `dropLabel?`, `dropSubtext?`, `onFilesSelected?` |
39
39
  | `FileThumbnail` | File thumbnail with MIME-aware icon fallback. Shows an `<img>` for image files (with loading/error states) and typed icons for PDF, ZIP, CAD, and generic files. Size variants: `sm`, `md`, `lg`, `xl`. | `name`, `mimeType`, `url?`, `size?` |
40
- | `SignaturePad` | Draw signature on canvas | `onSave: (dataUrl: string) => void` |
40
+ | `SignaturePad` | Draw signature on canvas (mouse + touch). Emits base64 PNG on each stroke-end via bindable `signatureData` or `onSignatureChange(data)`. Exposes `clear()`/`isEmpty()`. Pass `disabled` for a dimmed, non-interactive preview. | `signatureData` (bindable), `onSignatureChange`, `label`, `width`, `height`, `disabled`, `required` |
41
41
 
42
42
  ### Scheduling & Time
43
43
 
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Winke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -33,7 +33,7 @@
33
33
  * ─── Usage — action form (most sites) ─────────────────────────────────────────
34
34
  *
35
35
  * <script>
36
- * import { collapse } from 'jatui'
36
+ * import { collapse } from '../../../dist'
37
37
  * let collapsible = $state(false)
38
38
  * let expanded = $state(false)
39
39
  * const bodyId = `collapse-${$props.id()}`
@@ -33,7 +33,7 @@
33
33
  * ─── Usage — action form (most sites) ─────────────────────────────────────────
34
34
  *
35
35
  * <script>
36
- * import { collapse } from 'jatui'
36
+ * import { collapse } from '../../../dist'
37
37
  * let collapsible = $state(false)
38
38
  * let expanded = $state(false)
39
39
  * const bodyId = `collapse-${$props.id()}`
@@ -58,34 +58,22 @@
58
58
  * state (e.g. a `SvelteSet` of ids, with the id interpolating the row's own id) —
59
59
  * one action instance attaches per row element.
60
60
  */
61
- var __assign = (this && this.__assign) || function () {
62
- __assign = Object.assign || function(t) {
63
- for (var s, i = 1, n = arguments.length; i < n; i++) {
64
- s = arguments[i];
65
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
66
- t[p] = s[p];
67
- }
68
- return t;
69
- };
70
- return __assign.apply(this, arguments);
71
- };
72
- var DEFAULTS = { threshold: 120, expanded: false, fadeStart: 55, epsilon: 4 };
73
- export function collapse(node, options) {
74
- if (options === void 0) { options = {}; }
75
- var opts = __assign(__assign({}, DEFAULTS), options);
76
- var collapsible = false;
77
- var rafId = 0;
78
- var applied = false;
61
+ const DEFAULTS = { threshold: 120, expanded: false, fadeStart: 55, epsilon: 4 };
62
+ export function collapse(node, options = {}) {
63
+ let opts = { ...DEFAULTS, ...options };
64
+ let collapsible = false;
65
+ let rafId = 0;
66
+ let applied = false;
79
67
  function gradient() {
80
- return "linear-gradient(to bottom, black ".concat(opts.fadeStart, "%, transparent 100%)");
68
+ return `linear-gradient(to bottom, black ${opts.fadeStart}%, transparent 100%)`;
81
69
  }
82
70
  function apply() {
83
- var shouldCollapse = collapsible && !opts.expanded;
71
+ const shouldCollapse = collapsible && !opts.expanded;
84
72
  if (shouldCollapse === applied)
85
73
  return; // idempotent — avoids RO feedback loops
86
74
  applied = shouldCollapse;
87
75
  if (shouldCollapse) {
88
- node.style.maxHeight = "".concat(opts.threshold, "px");
76
+ node.style.maxHeight = `${opts.threshold}px`;
89
77
  node.style.overflow = 'hidden';
90
78
  node.style.maskImage = gradient();
91
79
  node.style.webkitMaskImage = gradient();
@@ -98,12 +86,11 @@ export function collapse(node, options) {
98
86
  }
99
87
  }
100
88
  function measure() {
101
- var _a;
102
89
  // scrollHeight is the full content height even while collapsed.
103
- var next = node.scrollHeight > opts.threshold + opts.epsilon;
90
+ const next = node.scrollHeight > opts.threshold + opts.epsilon;
104
91
  if (next !== collapsible) {
105
92
  collapsible = next;
106
- (_a = opts.onCollapsible) === null || _a === void 0 ? void 0 : _a.call(opts, collapsible);
93
+ opts.onCollapsible?.(collapsible);
107
94
  }
108
95
  apply();
109
96
  }
@@ -112,13 +99,12 @@ export function collapse(node, options) {
112
99
  rafId = requestAnimationFrame(measure);
113
100
  }
114
101
  // Re-measure when the content's natural size changes (streaming, {@html}, lazy images).
115
- var ro = new ResizeObserver(scheduleMeasure);
102
+ const ro = new ResizeObserver(scheduleMeasure);
116
103
  ro.observe(node);
117
104
  scheduleMeasure();
118
105
  return {
119
- update: function (next) {
120
- if (next === void 0) { next = {}; }
121
- opts = __assign(__assign({}, DEFAULTS), next);
106
+ update(next = {}) {
107
+ opts = { ...DEFAULTS, ...next };
122
108
  // expanded/threshold/fade may have changed — re-measure and let
123
109
  // apply() compare the freshly computed shouldCollapse against the
124
110
  // real last-applied state. (Force-flipping `applied` here used to make
@@ -126,7 +112,7 @@ export function collapse(node, options) {
126
112
  // silently skipping the style update.)
127
113
  scheduleMeasure();
128
114
  },
129
- destroy: function () {
115
+ destroy() {
130
116
  cancelAnimationFrame(rafId);
131
117
  ro.disconnect();
132
118
  },
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Shared keyboard-navigation action + composable for list-like UIs.
3
+ *
4
+ * Base motions — j/k / ArrowDown/ArrowUp with wraparound, Enter/Space to
5
+ * trigger select, Escape to clear focus. Typing targets (input, textarea,
6
+ * select, contenteditable) and most modifier combos are skipped so the
7
+ * shortcuts never interfere with typing or app-level keybindings.
8
+ *
9
+ * Vim-motion layer (always on):
10
+ *
11
+ * gg jump to first item
12
+ * G jump to last item
13
+ * {n}j / {n}k move `n` items (count-prefix buffer with 1.5s timeout)
14
+ * {n}G, {n}gg jump to 1-indexed line `n` (clamped to list length)
15
+ * Ctrl+D / Ctrl+U move roughly half a viewport's worth of items
16
+ * zz scroll the focused item to the centre of its viewport
17
+ *
18
+ * Selection layer (opt-in via `selectable: true`):
19
+ *
20
+ * V toggle visual mode (anchor at current focus, selects it)
21
+ * j / k while visual mode is on, also extends the range from anchor
22
+ * x toggle selection of the focused item (no movement)
23
+ * Shift+J / K toggle current + move down/up + toggle new focus (range-build)
24
+ * * toggle select-all of the currently visible items
25
+ * Escape backs out one layer at a time:
26
+ * pending motion → visual mode → selection → focus / onEscape
27
+ *
28
+ * Selected items get the `selected-class` (default `jk-selected`) so pages
29
+ * can style them. The controller exposes selectedIds()/clearSelection()/
30
+ * selectAll()/toggleSelection()/isSelecting() and fires onSelectionChange
31
+ * whenever the set mutates. A floating `BulkActionBar` is the canonical
32
+ * chip for surfacing the count + per-route bulk actions.
33
+ *
34
+ * Usage — action form (simplest):
35
+ *
36
+ * <div use:listNav={{
37
+ * onSelect: (el, idx) => openDetail(el.dataset.navId!),
38
+ * onEscape: () => closeDetail(),
39
+ * }}>
40
+ * {#each items as item}
41
+ * <button data-nav-id={item.id}>{item.title}</button>
42
+ * {/each}
43
+ * </div>
44
+ *
45
+ * Usage — composable form (when you already have a keydown handler):
46
+ *
47
+ * const nav = createListNav({
48
+ * getItems: () => Array.from(container.querySelectorAll('[data-nav-id]')),
49
+ * onSelect: (el) => openDetail(el.dataset.navId!),
50
+ * selectable: true,
51
+ * onSelectionChange: (ids) => { selectedIds = ids; },
52
+ * });
53
+ *
54
+ * function onKeydown(e: KeyboardEvent) {
55
+ * if (nav.handleKeydown(e)) return;
56
+ * // ...page-specific shortcuts
57
+ * }
58
+ */
59
+ import type { Action } from 'svelte/action';
60
+ export interface ListNavOptions {
61
+ /**
62
+ * Return the navigable HTML elements in list order. Called on every
63
+ * navigation event so reactive lists stay in sync. When omitted in the
64
+ * action form, the action queries the node itself using {@link itemSelector}.
65
+ */
66
+ getItems?: () => HTMLElement[] | ArrayLike<HTMLElement>;
67
+ /** CSS selector used by the action to locate items. Default: `[data-nav-id]`. */
68
+ itemSelector?: string;
69
+ /** Fired when Enter or Space is pressed on the focused item. */
70
+ onSelect?: (item: HTMLElement, index: number) => void;
71
+ /** Fired when Escape is pressed. Default behaviour: clears focus. */
72
+ onEscape?: () => void;
73
+ /** Fired whenever the focused index changes (including cleared focus). */
74
+ onFocusChange?: (item: HTMLElement | null, index: number) => void;
75
+ /** When true, j at the last item wraps to first (and vice-versa). Default: true. */
76
+ wraparound?: boolean;
77
+ /** Class applied to the currently focused item. Default: `jk-focused`. */
78
+ focusedClass?: string;
79
+ /** When false, all shortcuts no-op. Default: true. */
80
+ enabled?: boolean;
81
+ /** Action-only. When true, listen on window; when false, on the node. Default: true. */
82
+ global?: boolean;
83
+ /**
84
+ * Enable the selection layer (V / Shift+J / Shift+K / x / *). Default: false.
85
+ * When false, those keys pass through unhandled so pages can keep them.
86
+ */
87
+ selectable?: boolean;
88
+ /** Class applied to selected items. Default: `jk-selected`. */
89
+ selectedClass?: string;
90
+ /** Fires whenever the selection set mutates. Receives a fresh Set snapshot. */
91
+ onSelectionChange?: (ids: Set<string>) => void;
92
+ /**
93
+ * Resolve an item's stable id. Default: `el.dataset.navId`. Override if
94
+ * your items use a different attribute (e.g. `data-rule-nav-id`).
95
+ */
96
+ getItemId?: (el: HTMLElement) => string | null | undefined;
97
+ }
98
+ export interface KeyboardShortcut {
99
+ key: string;
100
+ description: string;
101
+ }
102
+ export interface ListNavController {
103
+ /** Returns true if the event was handled (preventDefault was called). */
104
+ handleKeydown(event: KeyboardEvent): boolean;
105
+ focusedIndex(): number;
106
+ focus(index: number): void;
107
+ focusFirst(): void;
108
+ focusLast(): void;
109
+ clear(): void;
110
+ /** Re-apply the focused class after items change (e.g. after a filter). */
111
+ refresh(): void;
112
+ /** Read-only snapshot of selected item ids. */
113
+ selectedIds(): Set<string>;
114
+ /** Toggle selection. With no arg, toggles the focused item. */
115
+ toggleSelection(navId?: string): void;
116
+ /** Replace the selection with the given ids (or clear if empty). */
117
+ setSelection(ids: Iterable<string>): void;
118
+ /** Drop the selection and exit visual mode. */
119
+ clearSelection(): void;
120
+ /** Toggle select-all of the currently visible items. */
121
+ selectAll(): void;
122
+ /** True while visual-mode (V) is active. */
123
+ isSelecting(): boolean;
124
+ }
125
+ /**
126
+ * Imperative composable — use when you already own a keydown handler and want
127
+ * listNav to decide whether to consume the event.
128
+ */
129
+ export declare function createListNav(options: ListNavOptions): ListNavController;
130
+ /**
131
+ * Svelte action — attaches a keydown listener (window by default) and handles
132
+ * j/k/ArrowDown/ArrowUp/Enter/Space/Escape navigation over children of `node`.
133
+ */
134
+ export declare const listNav: Action<HTMLElement, ListNavOptions | undefined>;