@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 +1 -1
- package/src/lib/actions/collapse.ts +134 -0
- package/src/lib/components/ChipInput.svelte +19 -12
- package/src/lib/components/Collapse.svelte +82 -0
- package/src/lib/components/InlineEdit.svelte +11 -7
- package/src/lib/components/linked-columns/LinkedColumns.svelte +15 -9
- package/src/lib/index.ts +3 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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';
|