@joewinke/jatui 0.1.20 → 0.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Tall-content collapse — Svelte action.
3
+ *
4
+ * Caps an arbitrarily-tall block at a max height with a fade-out gradient so it
5
+ * shows a preview instead of overflowing its container. The consumer renders the
6
+ * "Show more / Show less" toggle and owns the `expanded` boolean; the action does
7
+ * the measuring and applies the collapse styling.
8
+ *
9
+ * Why an action (not a wrapper-only component): most adoption sites are existing
10
+ * elements (`<p>`, a `prose` div, a `<ul>`, a `<pre>`), and an action attaches
11
+ * with zero DOM restructuring. A batteries-included `<Collapse>` wrapper that
12
+ * uses this action is also provided for greenfield use.
13
+ *
14
+ * ─── Why inline styles, not a CSS class ───────────────────────────────────────
15
+ *
16
+ * The action writes `max-height` / `overflow` / `mask-image` directly on the node
17
+ * when collapsed. That means it needs NO accompanying stylesheet — it works
18
+ * identically in Tailwind-utility components and scoped-CSS components, and
19
+ * consumers don't have to copy a `.collapse-*` rule into their app.css (the same
20
+ * trap rail.css has). Expanding clears the inline styles.
21
+ *
22
+ * ─── Why ResizeObserver, not a one-shot rAF ───────────────────────────────────
23
+ *
24
+ * Streaming chat answers and `{@html}` markdown bodies grow AFTER mount. A
25
+ * one-shot measurement on mount would miss them (and would wrongly hide the
26
+ * toggle on content that hasn't streamed in yet). A ResizeObserver re-measures
27
+ * whenever the content's natural height changes, so `collapsible` stays correct.
28
+ *
29
+ * `scrollHeight` reports the FULL content height even while `max-height` +
30
+ * `overflow:hidden` are applied, so the action can keep measuring without ever
31
+ * having to un-collapse first.
32
+ *
33
+ * ─── Usage — action form (most sites) ─────────────────────────────────────────
34
+ *
35
+ * <script>
36
+ * import { collapse } from 'jatui'
37
+ * let collapsible = $state(false)
38
+ * let expanded = $state(false)
39
+ * </script>
40
+ *
41
+ * <div use:collapse={{ expanded, onCollapsible: (c) => (collapsible = c) }}>
42
+ * {content}
43
+ * </div>
44
+ * {#if collapsible}
45
+ * <button onclick={() => (expanded = !expanded)}>
46
+ * {expanded ? 'Show less ↑' : 'Show more ↓'}
47
+ * </button>
48
+ * {/if}
49
+ *
50
+ * For mapped lists, give each row its own `expanded` / `collapsible` state
51
+ * (e.g. a `SvelteSet` of ids) — one action instance attaches per row element.
52
+ */
53
+
54
+ export interface CollapseOptions {
55
+ /** Max collapsed height in px. Content taller than this collapses. Default 120. */
56
+ threshold?: number;
57
+ /** Controlled expand state. When true, the node is never collapsed. Default false. */
58
+ expanded?: boolean;
59
+ /** Where the fade-to-transparent begins, as a %. Default 55. */
60
+ fadeStart?: number;
61
+ /** Slack (px) added to the threshold so a near-fit doesn't toggle. Default 4. */
62
+ epsilon?: number;
63
+ /**
64
+ * Called whenever collapsibility changes (content crosses the threshold).
65
+ * Drive the toggle button's visibility from this — it self-hides when the
66
+ * content is shorter than the threshold.
67
+ */
68
+ onCollapsible?: (collapsible: boolean) => void;
69
+ }
70
+
71
+ const DEFAULTS = { threshold: 120, expanded: false, fadeStart: 55, epsilon: 4 } as const;
72
+
73
+ export function collapse(node: HTMLElement, options: CollapseOptions = {}) {
74
+ let opts = { ...DEFAULTS, ...options };
75
+ let collapsible = false;
76
+ let rafId = 0;
77
+ let applied = false;
78
+
79
+ function gradient() {
80
+ return `linear-gradient(to bottom, black ${opts.fadeStart}%, transparent 100%)`;
81
+ }
82
+
83
+ function apply() {
84
+ const shouldCollapse = collapsible && !opts.expanded;
85
+ if (shouldCollapse === applied) return; // idempotent — avoids RO feedback loops
86
+ applied = shouldCollapse;
87
+ if (shouldCollapse) {
88
+ node.style.maxHeight = `${opts.threshold}px`;
89
+ node.style.overflow = 'hidden';
90
+ node.style.maskImage = gradient();
91
+ node.style.webkitMaskImage = gradient();
92
+ } else {
93
+ node.style.maxHeight = '';
94
+ node.style.overflow = '';
95
+ node.style.maskImage = '';
96
+ node.style.webkitMaskImage = '';
97
+ }
98
+ }
99
+
100
+ function measure() {
101
+ // scrollHeight is the full content height even while collapsed.
102
+ const next = node.scrollHeight > opts.threshold + opts.epsilon;
103
+ if (next !== collapsible) {
104
+ collapsible = next;
105
+ opts.onCollapsible?.(collapsible);
106
+ }
107
+ apply();
108
+ }
109
+
110
+ function scheduleMeasure() {
111
+ cancelAnimationFrame(rafId);
112
+ rafId = requestAnimationFrame(measure);
113
+ }
114
+
115
+ // Re-measure when the content's natural size changes (streaming, {@html}, lazy images).
116
+ const ro = new ResizeObserver(scheduleMeasure);
117
+ ro.observe(node);
118
+
119
+ scheduleMeasure();
120
+
121
+ return {
122
+ update(next: CollapseOptions = {}) {
123
+ opts = { ...DEFAULTS, ...next };
124
+ // expanded/threshold/fade may have changed — re-apply (and re-measure
125
+ // in case threshold moved across the content height).
126
+ applied = !applied; // force apply() to re-evaluate against new opts
127
+ scheduleMeasure();
128
+ },
129
+ destroy() {
130
+ cancelAnimationFrame(rafId);
131
+ ro.disconnect();
132
+ },
133
+ };
134
+ }
@@ -1,15 +1,7 @@
1
- <script lang="ts">
2
- /**
3
- * ChipInput - Generic contenteditable input with inline chips and autocomplete.
4
- *
5
- * Handles:
6
- * - Contenteditable div with placeholder
7
- * - Chip insertion/deletion (non-editable inline spans)
8
- * - Autocomplete dropdown with keyboard navigation
9
- * - Serialization (DOM -> text with chip markers)
10
- */
11
-
12
- // --- Types ---
1
+ <script module lang="ts">
2
+ // Public types — declared in the module script so that
3
+ // `import type { ChipSuggestion, SuggestionGroup, ChipInfo } from './ChipInput.svelte'`
4
+ // resolves. Types exported from the instance <script> are NOT module members.
13
5
  export interface ChipSuggestion {
14
6
  label: string;
15
7
  description?: string;
@@ -39,6 +31,21 @@
39
31
  /** If true, insert as plain text instead of a chip element */
40
32
  insertAsText?: boolean;
41
33
  }
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ /**
38
+ * ChipInput - Generic contenteditable input with inline chips and autocomplete.
39
+ *
40
+ * Handles:
41
+ * - Contenteditable div with placeholder
42
+ * - Chip insertion/deletion (non-editable inline spans)
43
+ * - Autocomplete dropdown with keyboard navigation
44
+ * - Serialization (DOM -> text with chip markers)
45
+ */
46
+
47
+ // --- Types (ChipSuggestion, SuggestionGroup, ChipInfo) are declared in the
48
+ // module <script> above so they are importable as module members. ---
42
49
 
43
50
  // --- Props ---
44
51
  let {
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Collapse — batteries-included tall-content collapser.
4
+ *
5
+ * Wraps children in a region that caps at `threshold` px with a fade-out
6
+ * gradient and renders a "Show more / Show less" toggle that self-hides when
7
+ * the content is shorter than the threshold. Uses the `collapse` action for
8
+ * measurement (ResizeObserver-backed, so streaming / {@html} content works).
9
+ *
10
+ * <Collapse>
11
+ * {@html renderedMarkdown}
12
+ * </Collapse>
13
+ *
14
+ * For existing elements you don't want to wrap (a bare `<p>` inside an
15
+ * `{#each}`, a `<pre>`), use the `collapse` action directly instead.
16
+ */
17
+ import { collapse } from '../actions/collapse';
18
+
19
+ interface Props {
20
+ /** Max collapsed height in px. Default 120. */
21
+ threshold?: number;
22
+ /** Where the fade begins, as a %. Default 55. */
23
+ fadeStart?: number;
24
+ /** Start expanded. Default false. */
25
+ expanded?: boolean;
26
+ /** Toggle label when collapsed. Default 'Show more ↓'. */
27
+ moreLabel?: string;
28
+ /** Toggle label when expanded. Default 'Show less ↑'. */
29
+ lessLabel?: string;
30
+ /** Extra classes on the content region. */
31
+ class?: string;
32
+ children?: import('svelte').Snippet;
33
+ }
34
+
35
+ let {
36
+ threshold = 120,
37
+ fadeStart = 55,
38
+ expanded = $bindable(false),
39
+ moreLabel = 'Show more ↓',
40
+ lessLabel = 'Show less ↑',
41
+ class: className = '',
42
+ children,
43
+ }: Props = $props();
44
+
45
+ let collapsible = $state(false);
46
+ </script>
47
+
48
+ <div
49
+ class={`jatui-collapse-body ${className}`.trim()}
50
+ use:collapse={{ threshold, fadeStart, expanded, onCollapsible: (c) => (collapsible = c) }}
51
+ >
52
+ {#if children}
53
+ {@render children()}
54
+ {/if}
55
+ </div>
56
+ {#if collapsible}
57
+ <button type="button" class="jatui-collapse-toggle" onclick={() => (expanded = !expanded)}>
58
+ {expanded ? lessLabel : moreLabel}
59
+ </button>
60
+ {/if}
61
+
62
+ <style>
63
+ .jatui-collapse-body {
64
+ position: relative;
65
+ }
66
+ .jatui-collapse-toggle {
67
+ display: block;
68
+ margin-top: 4px;
69
+ padding: 2px 0;
70
+ font-size: 11px;
71
+ color: var(--color-primary, oklch(0.65 0.12 250));
72
+ background: none;
73
+ border: none;
74
+ cursor: pointer;
75
+ opacity: 0.75;
76
+ transition: opacity 0.1s;
77
+ }
78
+ .jatui-collapse-toggle:hover {
79
+ opacity: 1;
80
+ text-decoration: underline;
81
+ }
82
+ </style>
@@ -1,3 +1,14 @@
1
+ <script module lang="ts">
2
+ // Declared in the module script so `import type { DisplaySegment } from
3
+ // './InlineEdit.svelte'` resolves (instance-script exports are not module members).
4
+ /** A display segment for formula-aware rendering */
5
+ export interface DisplaySegment {
6
+ type: 'text' | 'formula';
7
+ display: string;
8
+ tooltip?: string;
9
+ }
10
+ </script>
11
+
1
12
  <script lang="ts">
2
13
  /**
3
14
  * InlineEdit Component
@@ -14,13 +25,6 @@
14
25
  * - Optional formula-aware display: segments with type/display/tooltip
15
26
  */
16
27
 
17
- /** A display segment for formula-aware rendering */
18
- export interface DisplaySegment {
19
- type: 'text' | 'formula';
20
- display: string;
21
- tooltip?: string;
22
- }
23
-
24
28
  interface Props {
25
29
  /** Current value */
26
30
  value: string;
@@ -1,3 +1,17 @@
1
+ <script module lang="ts">
2
+ // Declared in the module script so `import type { ColumnLink } from
3
+ // './LinkedColumns.svelte'` resolves (instance-script exports are not module members).
4
+ /** Describes which left items a single right (anchor) item covers. */
5
+ export interface ColumnLink {
6
+ /** The right item's id this link belongs to. */
7
+ rightId: string | number;
8
+ /** First left item id in the covered range (inclusive). */
9
+ leftStartId: string | number;
10
+ /** Last left item id in the covered range (inclusive). */
11
+ leftEndId: string | number;
12
+ }
13
+ </script>
14
+
1
15
  <script lang="ts" generics="L extends { id: string | number }, R extends { id: string | number }">
2
16
  /**
3
17
  * LinkedColumns — generic two-column ink-up ribbon primitive.
@@ -51,15 +65,7 @@
51
65
 
52
66
  import type { Snippet } from 'svelte';
53
67
 
54
- /** Describes which left items a single right (anchor) item covers. */
55
- export interface ColumnLink {
56
- /** The right item's id this link belongs to. */
57
- rightId: string | number;
58
- /** First left item id in the covered range (inclusive). */
59
- leftStartId: string | number;
60
- /** Last left item id in the covered range (inclusive). */
61
- leftEndId: string | number;
62
- }
68
+ // ColumnLink is declared in the module <script> above so it's importable as a module member.
63
69
 
64
70
  let {
65
71
  leftItems,
package/src/lib/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  // Actions
2
2
  export { railNav, createRailNav, cycle } from './actions/railNav';
3
3
  export type { RailNavOptions, RailNavController } from './actions/railNav';
4
+ export { collapse } from './actions/collapse';
5
+ export type { CollapseOptions } from './actions/collapse';
6
+ export { default as Collapse } from './components/Collapse.svelte';
4
7
 
5
8
  // Components — Session primitives (from JAT IDE, jat-jj79f.16 G1)
6
9
  export { default as LinkedColumns } from './components/linked-columns/LinkedColumns.svelte';