@vit-foundation/ui 0.31.0 → 0.32.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/README.md +1 -0
- package/dist/components/madlib/InlineSelect.svelte +221 -0
- package/dist/components/madlib/InlineSelect.svelte.d.ts +49 -0
- package/dist/components/madlib/Madlib.svelte +264 -0
- package/dist/components/madlib/Madlib.svelte.d.ts +72 -0
- package/dist/components/madlib/sentenceTree.d.ts +92 -0
- package/dist/components/madlib/sentenceTree.js +140 -0
- package/dist/madlib.d.ts +17 -0
- package/dist/madlib.js +15 -0
- package/dist/styles/tokens.css +23 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Everything exports flat from the root, and again grouped by role:
|
|
|
34
34
|
| [`/community`](./docs/components/community.md) | AuthPageShell, LoginForm, SignupForm, GoogleAuthForm, AccountPanel, NewsletterSignup, CommentSection, ReactionBar, ContactForm |
|
|
35
35
|
| [`/admin`](./docs/components/admin.md) | DecorMosaic, PageHeading, Sidebar — shell-level composition for the foundation's internal tools |
|
|
36
36
|
| [`/scrolly`](./docs/components/scrolly.md) | ScrollySteps, ScrollyStepIndicator, CrossfadeVideo, GlassCard, the stepStyle ramp — the only entry point that does NOT re-export from the root, and the only one with a peer of its own |
|
|
37
|
+
| [`/madlib`](./docs/components/madlib.md) | Madlib, InlineSelect, the sentence-tree helpers (walkTree, completePath, defaultPath, replaceAt, leafPaths) — a sentence with blanks, themed with `--vit-madlib-*`; like `/scrolly`, not re-exported from the root |
|
|
37
38
|
| [`/edit`](./docs/edit-mode.md) | Editable, setEditAdapter(adapter, EDIT_CHROME)/getEditAdapter, descriptors and helpers, collectionEditing, LocalizedText |
|
|
38
39
|
| [`/config`](./docs/getting-started.md#wiring-an-app-uiprovider) | UiProvider, UiConfig, the locale set, the default Catalan messages |
|
|
39
40
|
| `/contract` | The component-free half: LOCALES, BASE_LOCALE, localize, REACTIONS, PAGE_COPY_KEYS and the edit-descriptor types — the one subpath a host may import from SERVER code |
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component InlineSelect
|
|
3
|
+
|
|
4
|
+
A select that sits inside running text: the chosen label, underlined, with a
|
|
5
|
+
small caret, and a listbox that drops below it. It inherits the font, size
|
|
6
|
+
and line-height of the text around it, which is the whole point — a native
|
|
7
|
+
`<select>` cannot be made to read as a word in a sentence.
|
|
8
|
+
|
|
9
|
+
It is the control `<Madlib>` renders for each blank, and it is exported on
|
|
10
|
+
its own for a lone blank in a heading ("Practices in [Spain]").
|
|
11
|
+
|
|
12
|
+
## Theming
|
|
13
|
+
Reads the madlib token family, with fallbacks, so it renders standalone:
|
|
14
|
+
|
|
15
|
+
| property | role |
|
|
16
|
+
|--------------------------------|----------------------------------------|
|
|
17
|
+
| `--vit-madlib-control-weight` | weight of the chosen label |
|
|
18
|
+
| `--vit-madlib-muted-color` | the unchosen options |
|
|
19
|
+
| `--vit-madlib-select-accent` | the underline while open or hovered |
|
|
20
|
+
| `--vit-madlib-menu-bg` | the listbox surface |
|
|
21
|
+
| `--vit-madlib-menu-hover` | the option under the pointer / chosen |
|
|
22
|
+
| `--vit-madlib-menu-shadow` | the listbox shadow |
|
|
23
|
+
| `--vit-madlib-menu-max-height` | the listbox scroll height |
|
|
24
|
+
| `--vit-madlib-menu-z` | the listbox stacking order |
|
|
25
|
+
|
|
26
|
+
@property value - The chosen option's value
|
|
27
|
+
@property options - The options offered, in order
|
|
28
|
+
@property onchange - Fired with the new value when an option is picked
|
|
29
|
+
@property onopen - Fired when the listbox is opened, before it shows
|
|
30
|
+
@property label - Accessible name for the control, when the surrounding text does not give one
|
|
31
|
+
@property class - Extra classes appended to the wrapper
|
|
32
|
+
-->
|
|
33
|
+
<script lang="ts" module>
|
|
34
|
+
/** One option row. `value` is what the control carries: a string, always. */
|
|
35
|
+
export interface InlineOption {
|
|
36
|
+
value: string;
|
|
37
|
+
label: string;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { fly } from 'svelte/transition';
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
value,
|
|
47
|
+
options,
|
|
48
|
+
onchange,
|
|
49
|
+
onopen,
|
|
50
|
+
label,
|
|
51
|
+
class: className = ''
|
|
52
|
+
}: {
|
|
53
|
+
value: string;
|
|
54
|
+
options: readonly InlineOption[];
|
|
55
|
+
onchange?: (value: string) => void;
|
|
56
|
+
onopen?: () => void;
|
|
57
|
+
label?: string;
|
|
58
|
+
class?: string;
|
|
59
|
+
} = $props();
|
|
60
|
+
|
|
61
|
+
let open = $state(false);
|
|
62
|
+
let wrapper: HTMLElement | undefined = $state();
|
|
63
|
+
|
|
64
|
+
const chosen = $derived(options.find((option) => option.value === value)?.label ?? '');
|
|
65
|
+
|
|
66
|
+
function toggle() {
|
|
67
|
+
if (!open) onopen?.();
|
|
68
|
+
open = !open;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pick(option: InlineOption) {
|
|
72
|
+
if (option.disabled) return;
|
|
73
|
+
open = false;
|
|
74
|
+
onchange?.(option.value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onkeydown(event: KeyboardEvent) {
|
|
78
|
+
if (event.key === 'Escape') {
|
|
79
|
+
open = false;
|
|
80
|
+
} else if (event.key === 'ArrowDown' && !open) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
toggle();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Any click that lands outside the control closes it. */
|
|
87
|
+
function onwindowclick(event: MouseEvent) {
|
|
88
|
+
if (open && wrapper && !wrapper.contains(event.target as Node)) open = false;
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<svelte:window onclick={onwindowclick} />
|
|
93
|
+
|
|
94
|
+
<span bind:this={wrapper} class="vit-inline-select {className}">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="vit-inline-select__trigger"
|
|
98
|
+
class:vit-inline-select__trigger--open={open}
|
|
99
|
+
aria-haspopup="listbox"
|
|
100
|
+
aria-expanded={open}
|
|
101
|
+
aria-label={label}
|
|
102
|
+
onclick={toggle}
|
|
103
|
+
{onkeydown}
|
|
104
|
+
>
|
|
105
|
+
<!-- The trigger is as wide as the CHOSEN label only, so the sentence
|
|
106
|
+
reflows as the value changes. The menu carries the widest option. -->
|
|
107
|
+
<span class="vit-inline-select__value">{chosen}</span>
|
|
108
|
+
<svg class="vit-inline-select__caret" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
109
|
+
<path
|
|
110
|
+
d="M2.5 4.5L6 8L9.5 4.5"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
stroke-width="1.5"
|
|
113
|
+
stroke-linecap="round"
|
|
114
|
+
stroke-linejoin="round"
|
|
115
|
+
/>
|
|
116
|
+
</svg>
|
|
117
|
+
</button>
|
|
118
|
+
|
|
119
|
+
{#if open}
|
|
120
|
+
<ul class="vit-inline-select__menu" role="listbox" transition:fly={{ y: -8, duration: 150 }}>
|
|
121
|
+
{#each options as option (option.value)}
|
|
122
|
+
<li
|
|
123
|
+
role="option"
|
|
124
|
+
aria-selected={option.value === value}
|
|
125
|
+
aria-disabled={option.disabled || undefined}
|
|
126
|
+
class="vit-inline-select__option"
|
|
127
|
+
class:vit-inline-select__option--chosen={option.value === value}
|
|
128
|
+
class:vit-inline-select__option--disabled={option.disabled}
|
|
129
|
+
tabindex={option.disabled ? -1 : 0}
|
|
130
|
+
onclick={() => pick(option)}
|
|
131
|
+
onkeydown={(event) => event.key === 'Enter' && pick(option)}
|
|
132
|
+
>
|
|
133
|
+
{option.label}
|
|
134
|
+
</li>
|
|
135
|
+
{/each}
|
|
136
|
+
</ul>
|
|
137
|
+
{/if}
|
|
138
|
+
</span>
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
.vit-inline-select {
|
|
142
|
+
position: relative;
|
|
143
|
+
display: inline-block;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.vit-inline-select__trigger {
|
|
147
|
+
display: inline;
|
|
148
|
+
padding: 0 0 0.1em;
|
|
149
|
+
margin: 0;
|
|
150
|
+
border: 0;
|
|
151
|
+
border-bottom: 2px solid currentColor;
|
|
152
|
+
background: transparent;
|
|
153
|
+
color: inherit;
|
|
154
|
+
font: inherit;
|
|
155
|
+
font-weight: var(--vit-madlib-control-weight, 700);
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: border-color 200ms;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.vit-inline-select__trigger:hover,
|
|
161
|
+
.vit-inline-select__trigger--open {
|
|
162
|
+
border-bottom-color: var(--vit-madlib-select-accent, currentColor);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.vit-inline-select__value {
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.vit-inline-select__caret {
|
|
170
|
+
display: inline;
|
|
171
|
+
width: 0.5em;
|
|
172
|
+
height: 0.5em;
|
|
173
|
+
margin-left: 0.125em;
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
transition: transform 200ms;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.vit-inline-select__trigger--open .vit-inline-select__caret {
|
|
179
|
+
transform: rotate(180deg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.vit-inline-select__menu {
|
|
183
|
+
position: absolute;
|
|
184
|
+
left: 0;
|
|
185
|
+
z-index: var(--vit-madlib-menu-z, 40);
|
|
186
|
+
margin: 0.25em 0 0;
|
|
187
|
+
padding: 0.25em 0;
|
|
188
|
+
list-style: none;
|
|
189
|
+
width: max-content;
|
|
190
|
+
max-width: 42rem;
|
|
191
|
+
max-height: var(--vit-madlib-menu-max-height, 15rem);
|
|
192
|
+
overflow: auto;
|
|
193
|
+
background: var(--vit-madlib-menu-bg, #ffffff);
|
|
194
|
+
box-shadow: var(--vit-madlib-menu-shadow, none);
|
|
195
|
+
font: inherit;
|
|
196
|
+
scrollbar-width: thin;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.vit-inline-select__option {
|
|
200
|
+
padding: 0.375em 1em;
|
|
201
|
+
font-weight: var(--vit-madlib-control-weight, 700);
|
|
202
|
+
color: var(--vit-madlib-muted-color, color-mix(in srgb, currentColor 35%, transparent));
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
transition: background-color 150ms;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.vit-inline-select__option:hover,
|
|
208
|
+
.vit-inline-select__option:focus-visible {
|
|
209
|
+
background: var(--vit-madlib-menu-hover, color-mix(in srgb, currentColor 10%, transparent));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.vit-inline-select__option--chosen {
|
|
213
|
+
color: inherit;
|
|
214
|
+
background: var(--vit-madlib-menu-hover, color-mix(in srgb, currentColor 10%, transparent));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.vit-inline-select__option--disabled {
|
|
218
|
+
opacity: 0.4;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
}
|
|
221
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** One option row. `value` is what the control carries: a string, always. */
|
|
2
|
+
export interface InlineOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
value: string;
|
|
9
|
+
options: readonly InlineOption[];
|
|
10
|
+
onchange?: (value: string) => void;
|
|
11
|
+
onopen?: () => void;
|
|
12
|
+
label?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* InlineSelect
|
|
17
|
+
*
|
|
18
|
+
* A select that sits inside running text: the chosen label, underlined, with a
|
|
19
|
+
* small caret, and a listbox that drops below it. It inherits the font, size
|
|
20
|
+
* and line-height of the text around it, which is the whole point — a native
|
|
21
|
+
* `<select>` cannot be made to read as a word in a sentence.
|
|
22
|
+
*
|
|
23
|
+
* It is the control `<Madlib>` renders for each blank, and it is exported on
|
|
24
|
+
* its own for a lone blank in a heading ("Practices in [Spain]").
|
|
25
|
+
*
|
|
26
|
+
* ## Theming
|
|
27
|
+
* Reads the madlib token family, with fallbacks, so it renders standalone:
|
|
28
|
+
*
|
|
29
|
+
* | property | role |
|
|
30
|
+
* |--------------------------------|----------------------------------------|
|
|
31
|
+
* | `--vit-madlib-control-weight` | weight of the chosen label |
|
|
32
|
+
* | `--vit-madlib-muted-color` | the unchosen options |
|
|
33
|
+
* | `--vit-madlib-select-accent` | the underline while open or hovered |
|
|
34
|
+
* | `--vit-madlib-menu-bg` | the listbox surface |
|
|
35
|
+
* | `--vit-madlib-menu-hover` | the option under the pointer / chosen |
|
|
36
|
+
* | `--vit-madlib-menu-shadow` | the listbox shadow |
|
|
37
|
+
* | `--vit-madlib-menu-max-height` | the listbox scroll height |
|
|
38
|
+
* | `--vit-madlib-menu-z` | the listbox stacking order |
|
|
39
|
+
*
|
|
40
|
+
* @property value - The chosen option's value
|
|
41
|
+
* @property options - The options offered, in order
|
|
42
|
+
* @property onchange - Fired with the new value when an option is picked
|
|
43
|
+
* @property onopen - Fired when the listbox is opened, before it shows
|
|
44
|
+
* @property label - Accessible name for the control, when the surrounding text does not give one
|
|
45
|
+
* @property class - Extra classes appended to the wrapper
|
|
46
|
+
*/
|
|
47
|
+
declare const InlineSelect: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
48
|
+
type InlineSelect = ReturnType<typeof InlineSelect>;
|
|
49
|
+
export default InlineSelect;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Madlib
|
|
3
|
+
|
|
4
|
+
A sentence with blanks, each blank a control: "Show me [open positions] in
|
|
5
|
+
[Barcelona] over the last [12 months]". The blanks are the levels of a
|
|
6
|
+
{@link ./sentenceTree} — what a level offers depends on what was chosen
|
|
7
|
+
above it — and the value is the **path** of chosen ids, root to leaf.
|
|
8
|
+
|
|
9
|
+
The component owns nothing but the projection. `walkTree` turns the path into
|
|
10
|
+
one level per blank; `replaceAt` turns a pick into the next full path,
|
|
11
|
+
keeping the deeper choices that still apply; the host receives that path in
|
|
12
|
+
`onchange` and decides what it means. It never holds a copy of the path, so
|
|
13
|
+
a host that rejects or rewrites a change simply does not pass it back.
|
|
14
|
+
|
|
15
|
+
## Controls
|
|
16
|
+
Every blank is an {@link ./InlineSelect} unless `controls` says otherwise. A
|
|
17
|
+
level rendered as a `toggle` lists its options in the text — active one bold
|
|
18
|
+
and underlined, the others muted, separated by bars — and takes a line of its
|
|
19
|
+
own together with the text before it, so a two-way branch can read as a
|
|
20
|
+
headline over the rest of the sentence:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Farming in the present | future
|
|
24
|
+
Show me where [all crops] are grown, by [area]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Theming
|
|
28
|
+
CSS custom properties with plain fallbacks; the font, size and weight of the
|
|
29
|
+
text around each control are inherited by the control:
|
|
30
|
+
|
|
31
|
+
| property | role |
|
|
32
|
+
|---------------------------------|---------------------------------------------|
|
|
33
|
+
| `--vit-madlib-font` | the sentence's font family |
|
|
34
|
+
| `--vit-madlib-color` | the sentence's colour |
|
|
35
|
+
| `--vit-madlib-lead-size` | font size of a toggle's line |
|
|
36
|
+
| `--vit-madlib-lead-line-height` | line height of a toggle's line |
|
|
37
|
+
| `--vit-madlib-size` | font size of the dropdown lines |
|
|
38
|
+
| `--vit-madlib-line-height` | line height of the dropdown lines |
|
|
39
|
+
| `--vit-madlib-weight` | weight of the running text |
|
|
40
|
+
| `--vit-madlib-control-weight` | weight of a chosen option |
|
|
41
|
+
| `--vit-madlib-muted-color` | unchosen toggle options, bars, menu options |
|
|
42
|
+
| `--vit-madlib-accent` | the active toggle option's underline |
|
|
43
|
+
|
|
44
|
+
The `InlineSelect` tokens (`--vit-madlib-select-accent`, `--vit-madlib-menu-*`)
|
|
45
|
+
apply to every dropdown in the sentence.
|
|
46
|
+
|
|
47
|
+
@property tree - The sentence tree
|
|
48
|
+
@property path - The chosen node ids, root to leaf
|
|
49
|
+
@property onchange - Fired with the next full path when any blank changes
|
|
50
|
+
@property controls - How each depth is rendered; `'dropdown'` when omitted
|
|
51
|
+
@property onopen - Fired when any control is activated, before the change (a
|
|
52
|
+
host cycling through sentences stops here)
|
|
53
|
+
@property contentWidth - Bindable: the widest rendered line, in px, remeasured on every change
|
|
54
|
+
@property class - Extra classes appended to the wrapper
|
|
55
|
+
-->
|
|
56
|
+
<script lang="ts" module>
|
|
57
|
+
import type { SentenceLevel, SentenceNode } from './sentenceTree.js';
|
|
58
|
+
|
|
59
|
+
/** How one level of the sentence is rendered. */
|
|
60
|
+
export type MadlibControl = 'dropdown' | 'toggle';
|
|
61
|
+
|
|
62
|
+
/** Picks the control for a level. Receives the depth and the resolved level. */
|
|
63
|
+
export type MadlibControls = (depth: number, level: SentenceLevel) => MadlibControl;
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<script lang="ts">
|
|
67
|
+
import InlineSelect from './InlineSelect.svelte';
|
|
68
|
+
import { replaceAt, walkTree } from './sentenceTree.js';
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
tree,
|
|
72
|
+
path,
|
|
73
|
+
onchange,
|
|
74
|
+
controls,
|
|
75
|
+
onopen,
|
|
76
|
+
contentWidth = $bindable(0),
|
|
77
|
+
class: className = ''
|
|
78
|
+
}: {
|
|
79
|
+
tree: SentenceNode;
|
|
80
|
+
path: readonly string[];
|
|
81
|
+
onchange: (path: string[]) => void;
|
|
82
|
+
controls?: MadlibControls;
|
|
83
|
+
onopen?: () => void;
|
|
84
|
+
contentWidth?: number;
|
|
85
|
+
class?: string;
|
|
86
|
+
} = $props();
|
|
87
|
+
|
|
88
|
+
/** One blank of the sentence, with the text that introduces it. */
|
|
89
|
+
interface Blank {
|
|
90
|
+
depth: number;
|
|
91
|
+
level: SentenceLevel;
|
|
92
|
+
control: MadlibControl;
|
|
93
|
+
/** The root's label for the first blank, the previous node's connector after. */
|
|
94
|
+
lead: string | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A rendered line: a toggle stands alone on a lead line; dropdowns flow together. */
|
|
98
|
+
interface Line {
|
|
99
|
+
kind: 'lead' | 'body';
|
|
100
|
+
blanks: Blank[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const levels = $derived(walkTree(tree, path));
|
|
104
|
+
|
|
105
|
+
const lines = $derived.by((): Line[] => {
|
|
106
|
+
const out: Line[] = [];
|
|
107
|
+
levels.forEach((level, depth) => {
|
|
108
|
+
const blank: Blank = {
|
|
109
|
+
depth,
|
|
110
|
+
level,
|
|
111
|
+
control: controls?.(depth, level) ?? 'dropdown',
|
|
112
|
+
lead: depth === 0 ? tree.label : levels[depth - 1].node.connector
|
|
113
|
+
};
|
|
114
|
+
const last = out[out.length - 1];
|
|
115
|
+
if (blank.control === 'toggle') out.push({ kind: 'lead', blanks: [blank] });
|
|
116
|
+
else if (last?.kind === 'body') last.blanks.push(blank);
|
|
117
|
+
else out.push({ kind: 'body', blanks: [blank] });
|
|
118
|
+
});
|
|
119
|
+
return out;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A leaf's own `connector` has no next control to introduce, so it trails
|
|
124
|
+
* the sentence rather than being dropped.
|
|
125
|
+
*/
|
|
126
|
+
const trailing = $derived.by(() => {
|
|
127
|
+
const last = levels[levels.length - 1];
|
|
128
|
+
return last && !last.node.children?.length ? last.node.connector : undefined;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let root: HTMLElement | undefined = $state();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The space between a control and the text after it — none when that text
|
|
135
|
+
* opens with punctuation (", under a" hugs the control it follows).
|
|
136
|
+
* @param text - The connector or leading text about to be rendered
|
|
137
|
+
* @returns A single space, or nothing before punctuation
|
|
138
|
+
*/
|
|
139
|
+
const gap = (text: string) => (/^[,.;:!?)]/.test(text) ? '' : ' ');
|
|
140
|
+
|
|
141
|
+
function pick(depth: number, id: string) {
|
|
142
|
+
onchange(replaceAt(tree, path, depth, id));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function activate(depth: number, id: string) {
|
|
146
|
+
onopen?.();
|
|
147
|
+
pick(depth, id);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The widest rendered line: inline rects grouped by their row. A host
|
|
152
|
+
* aligning something under the sentence (a legend, a caption) wants the
|
|
153
|
+
* text's width, not the block's.
|
|
154
|
+
*/
|
|
155
|
+
function measure() {
|
|
156
|
+
if (!root) return;
|
|
157
|
+
const range = document.createRange();
|
|
158
|
+
range.selectNodeContents(root);
|
|
159
|
+
const left = root.getBoundingClientRect().left;
|
|
160
|
+
const rows: Record<number, number> = {};
|
|
161
|
+
for (const rect of range.getClientRects()) {
|
|
162
|
+
const row = Math.round(rect.top);
|
|
163
|
+
rows[row] = Math.max(rows[row] ?? 0, rect.right - left);
|
|
164
|
+
}
|
|
165
|
+
contentWidth = Math.ceil(Math.max(0, ...Object.values(rows)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
$effect(() => {
|
|
169
|
+
void path;
|
|
170
|
+
const frame = requestAnimationFrame(measure);
|
|
171
|
+
return () => cancelAnimationFrame(frame);
|
|
172
|
+
});
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<div bind:this={root} class="vit-madlib {className}">
|
|
176
|
+
{#if levels.length === 0}
|
|
177
|
+
<span class="vit-madlib__line vit-madlib__line--lead">{tree.label}</span>
|
|
178
|
+
{/if}
|
|
179
|
+
{#each lines as line, i (i)}
|
|
180
|
+
<span class="vit-madlib__line vit-madlib__line--{line.kind}">
|
|
181
|
+
{#each line.blanks as blank (blank.depth)}
|
|
182
|
+
{#if blank.lead}<span class="vit-madlib__text">{`${gap(blank.lead)}${blank.lead} `}</span
|
|
183
|
+
>{/if}
|
|
184
|
+
{#if blank.control === 'toggle'}
|
|
185
|
+
{#each blank.level.siblings as option, j (option.id)}
|
|
186
|
+
{#if j > 0}<span class="vit-madlib__separator" aria-hidden="true">|</span>{/if}
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="vit-madlib__toggle"
|
|
190
|
+
class:vit-madlib__toggle--active={option.id === blank.level.node.id}
|
|
191
|
+
aria-pressed={option.id === blank.level.node.id}
|
|
192
|
+
onclick={() => activate(blank.depth, option.id)}
|
|
193
|
+
>
|
|
194
|
+
{option.label}
|
|
195
|
+
</button>
|
|
196
|
+
{/each}
|
|
197
|
+
{:else}
|
|
198
|
+
<InlineSelect
|
|
199
|
+
value={blank.level.node.id}
|
|
200
|
+
options={blank.level.siblings.map((s) => ({ value: s.id, label: s.label }))}
|
|
201
|
+
onchange={(id) => pick(blank.depth, id)}
|
|
202
|
+
{onopen}
|
|
203
|
+
/>
|
|
204
|
+
{/if}
|
|
205
|
+
{/each}
|
|
206
|
+
{#if line === lines[lines.length - 1] && trailing}
|
|
207
|
+
<span class="vit-madlib__text">{`${gap(trailing)}${trailing}`}</span>
|
|
208
|
+
{/if}
|
|
209
|
+
</span>
|
|
210
|
+
{/each}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<style>
|
|
214
|
+
.vit-madlib {
|
|
215
|
+
font-family: var(--vit-madlib-font, inherit);
|
|
216
|
+
color: var(--vit-madlib-color, inherit);
|
|
217
|
+
font-weight: var(--vit-madlib-weight, inherit);
|
|
218
|
+
text-wrap: balance;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.vit-madlib__line {
|
|
222
|
+
display: block;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.vit-madlib__line--lead {
|
|
226
|
+
font-size: var(--vit-madlib-lead-size, 1em);
|
|
227
|
+
line-height: var(--vit-madlib-lead-line-height, 1.4);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.vit-madlib__line--body {
|
|
231
|
+
font-size: var(--vit-madlib-size, 1em);
|
|
232
|
+
line-height: var(--vit-madlib-line-height, 1.4);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.vit-madlib__separator {
|
|
236
|
+
padding: 0 0.375em;
|
|
237
|
+
color: var(--vit-madlib-muted-color, color-mix(in srgb, currentColor 35%, transparent));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.vit-madlib__toggle {
|
|
241
|
+
padding: 0 0 0.125em;
|
|
242
|
+
margin: 0;
|
|
243
|
+
border: 0;
|
|
244
|
+
border-bottom: 2px solid transparent;
|
|
245
|
+
background: transparent;
|
|
246
|
+
font: inherit;
|
|
247
|
+
color: var(--vit-madlib-muted-color, color-mix(in srgb, currentColor 35%, transparent));
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
transition: color 200ms;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.vit-madlib__toggle:hover {
|
|
253
|
+
color: inherit;
|
|
254
|
+
opacity: 0.6;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.vit-madlib__toggle--active,
|
|
258
|
+
.vit-madlib__toggle--active:hover {
|
|
259
|
+
color: inherit;
|
|
260
|
+
opacity: 1;
|
|
261
|
+
font-weight: var(--vit-madlib-control-weight, 700);
|
|
262
|
+
border-bottom-color: var(--vit-madlib-accent, currentColor);
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SentenceLevel, SentenceNode } from './sentenceTree.js';
|
|
2
|
+
/** How one level of the sentence is rendered. */
|
|
3
|
+
export type MadlibControl = 'dropdown' | 'toggle';
|
|
4
|
+
/** Picks the control for a level. Receives the depth and the resolved level. */
|
|
5
|
+
export type MadlibControls = (depth: number, level: SentenceLevel) => MadlibControl;
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
tree: SentenceNode;
|
|
8
|
+
path: readonly string[];
|
|
9
|
+
onchange: (path: string[]) => void;
|
|
10
|
+
controls?: MadlibControls;
|
|
11
|
+
onopen?: () => void;
|
|
12
|
+
contentWidth?: number;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Madlib
|
|
17
|
+
*
|
|
18
|
+
* A sentence with blanks, each blank a control: "Show me [open positions] in
|
|
19
|
+
* [Barcelona] over the last [12 months]". The blanks are the levels of a
|
|
20
|
+
* {@link ./sentenceTree} — what a level offers depends on what was chosen
|
|
21
|
+
* above it — and the value is the **path** of chosen ids, root to leaf.
|
|
22
|
+
*
|
|
23
|
+
* The component owns nothing but the projection. `walkTree` turns the path into
|
|
24
|
+
* one level per blank; `replaceAt` turns a pick into the next full path,
|
|
25
|
+
* keeping the deeper choices that still apply; the host receives that path in
|
|
26
|
+
* `onchange` and decides what it means. It never holds a copy of the path, so
|
|
27
|
+
* a host that rejects or rewrites a change simply does not pass it back.
|
|
28
|
+
*
|
|
29
|
+
* ## Controls
|
|
30
|
+
* Every blank is an {@link ./InlineSelect} unless `controls` says otherwise. A
|
|
31
|
+
* level rendered as a `toggle` lists its options in the text — active one bold
|
|
32
|
+
* and underlined, the others muted, separated by bars — and takes a line of its
|
|
33
|
+
* own together with the text before it, so a two-way branch can read as a
|
|
34
|
+
* headline over the rest of the sentence:
|
|
35
|
+
*
|
|
36
|
+
* ```
|
|
37
|
+
* Farming in the present | future
|
|
38
|
+
* Show me where [all crops] are grown, by [area]
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ## Theming
|
|
42
|
+
* CSS custom properties with plain fallbacks; the font, size and weight of the
|
|
43
|
+
* text around each control are inherited by the control:
|
|
44
|
+
*
|
|
45
|
+
* | property | role |
|
|
46
|
+
* |---------------------------------|---------------------------------------------|
|
|
47
|
+
* | `--vit-madlib-font` | the sentence's font family |
|
|
48
|
+
* | `--vit-madlib-color` | the sentence's colour |
|
|
49
|
+
* | `--vit-madlib-lead-size` | font size of a toggle's line |
|
|
50
|
+
* | `--vit-madlib-lead-line-height` | line height of a toggle's line |
|
|
51
|
+
* | `--vit-madlib-size` | font size of the dropdown lines |
|
|
52
|
+
* | `--vit-madlib-line-height` | line height of the dropdown lines |
|
|
53
|
+
* | `--vit-madlib-weight` | weight of the running text |
|
|
54
|
+
* | `--vit-madlib-control-weight` | weight of a chosen option |
|
|
55
|
+
* | `--vit-madlib-muted-color` | unchosen toggle options, bars, menu options |
|
|
56
|
+
* | `--vit-madlib-accent` | the active toggle option's underline |
|
|
57
|
+
*
|
|
58
|
+
* The `InlineSelect` tokens (`--vit-madlib-select-accent`, `--vit-madlib-menu-*`)
|
|
59
|
+
* apply to every dropdown in the sentence.
|
|
60
|
+
*
|
|
61
|
+
* @property tree - The sentence tree
|
|
62
|
+
* @property path - The chosen node ids, root to leaf
|
|
63
|
+
* @property onchange - Fired with the next full path when any blank changes
|
|
64
|
+
* @property controls - How each depth is rendered; `'dropdown'` when omitted
|
|
65
|
+
* @property onopen - Fired when any control is activated, before the change (a
|
|
66
|
+
* host cycling through sentences stops here)
|
|
67
|
+
* @property contentWidth - Bindable: the widest rendered line, in px, remeasured on every change
|
|
68
|
+
* @property class - Extra classes appended to the wrapper
|
|
69
|
+
*/
|
|
70
|
+
declare const Madlib: import("svelte").Component<$$ComponentProps, {}, "contentWidth">;
|
|
71
|
+
type Madlib = ReturnType<typeof Madlib>;
|
|
72
|
+
export default Madlib;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentence tree: the data behind a madlib, and the path arithmetic on it.
|
|
3
|
+
*
|
|
4
|
+
* A madlib is a sentence with blanks — "Show me [open positions] in
|
|
5
|
+
* [Barcelona] over the last [12 months]" — where each blank is one level of a
|
|
6
|
+
* tree and the options offered at a level are the children of what was picked
|
|
7
|
+
* at the level above. A selection is therefore a **path**: one node id per
|
|
8
|
+
* level, root to leaf. Everything the component does is arithmetic on that
|
|
9
|
+
* path, so it lives here, pure and tested, and `<Madlib>` only projects it.
|
|
10
|
+
*
|
|
11
|
+
* The tree is plain data. `label` is what a node reads as when it is the
|
|
12
|
+
* chosen option; `connector` is the text that follows it before the next
|
|
13
|
+
* blank; the root's `label` is the sentence's leading text. Nothing here
|
|
14
|
+
* knows what a path *means* — that is the host's mapping, on the way out.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* One node of a sentence tree.
|
|
18
|
+
*
|
|
19
|
+
* @property id - Unique among its siblings; the path carries these
|
|
20
|
+
* @property label - What the option reads as, in the control and in the sentence
|
|
21
|
+
* @property connector - Text after this node's control, before the next one. On a
|
|
22
|
+
* leaf it trails the sentence instead, since there is no next control
|
|
23
|
+
* @property children - The next level's options. A node without children is a leaf
|
|
24
|
+
*/
|
|
25
|
+
export interface SentenceNode {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
connector?: string;
|
|
29
|
+
children?: SentenceNode[];
|
|
30
|
+
}
|
|
31
|
+
/** One resolved level of a path: the node chosen there, and every option it was chosen from. */
|
|
32
|
+
export interface SentenceLevel {
|
|
33
|
+
node: SentenceNode;
|
|
34
|
+
siblings: SentenceNode[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolves a path against the tree, one level per id, stopping at the first
|
|
38
|
+
* id that is not a child of the level above (or at a leaf). The result is what
|
|
39
|
+
* a control per level needs: the chosen node and its siblings.
|
|
40
|
+
*
|
|
41
|
+
* @param tree - The root node
|
|
42
|
+
* @param path - Node ids, root to leaf
|
|
43
|
+
* @returns One entry per resolved level, possibly fewer than `path.length`
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* walkTree(tree, ['jobs', 'bcn'])
|
|
47
|
+
* // → [{ node: jobs, siblings: [jobs, projects] }, { node: bcn, siblings: [bcn, remote] }]
|
|
48
|
+
*/
|
|
49
|
+
export declare function walkTree(tree: SentenceNode, path: readonly string[]): SentenceLevel[];
|
|
50
|
+
/**
|
|
51
|
+
* Completes a partial path down to a leaf by taking the first child at every
|
|
52
|
+
* remaining level. The prefix is kept as far as it resolves; from the first
|
|
53
|
+
* id that does not, the defaults take over. `completePath(tree, [])` is the
|
|
54
|
+
* tree's default sentence.
|
|
55
|
+
*
|
|
56
|
+
* @param tree - The root node
|
|
57
|
+
* @param partial - Leading node ids, possibly empty, possibly partly invalid
|
|
58
|
+
* @returns A full root-to-leaf path
|
|
59
|
+
*/
|
|
60
|
+
export declare function completePath(tree: SentenceNode, partial: readonly string[]): string[];
|
|
61
|
+
/**
|
|
62
|
+
* The tree's default sentence: first child at every level.
|
|
63
|
+
*
|
|
64
|
+
* @param tree - The root node
|
|
65
|
+
* @returns A full root-to-leaf path, empty for a tree with no children
|
|
66
|
+
*/
|
|
67
|
+
export declare function defaultPath(tree: SentenceNode): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Changes one level of a path and re-completes everything below it.
|
|
70
|
+
*
|
|
71
|
+
* Levels above `depth` are kept. Below it, each level prefers the id the
|
|
72
|
+
* previous path had at that same depth when it is still a valid option there
|
|
73
|
+
* — so switching "maize" to "wheat" keeps "by area" — and falls back to the
|
|
74
|
+
* first child when it is not, which is what happens across a branch change or
|
|
75
|
+
* when the levels shift.
|
|
76
|
+
*
|
|
77
|
+
* @param tree - The root node
|
|
78
|
+
* @param path - The current path
|
|
79
|
+
* @param depth - Index of the level being changed
|
|
80
|
+
* @param id - The new node id at that level
|
|
81
|
+
* @returns A full root-to-leaf path
|
|
82
|
+
*/
|
|
83
|
+
export declare function replaceAt(tree: SentenceNode, path: readonly string[], depth: number, id: string): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Every root-to-leaf path, depth first, in the order the tree declares them.
|
|
86
|
+
* A host uses it to cycle through all sentences, or to check that each one
|
|
87
|
+
* resolves to something.
|
|
88
|
+
*
|
|
89
|
+
* @param tree - The root node
|
|
90
|
+
* @returns All full paths; `[[]]` for a tree with no children
|
|
91
|
+
*/
|
|
92
|
+
export declare function leafPaths(tree: SentenceNode): string[][];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentence tree: the data behind a madlib, and the path arithmetic on it.
|
|
3
|
+
*
|
|
4
|
+
* A madlib is a sentence with blanks — "Show me [open positions] in
|
|
5
|
+
* [Barcelona] over the last [12 months]" — where each blank is one level of a
|
|
6
|
+
* tree and the options offered at a level are the children of what was picked
|
|
7
|
+
* at the level above. A selection is therefore a **path**: one node id per
|
|
8
|
+
* level, root to leaf. Everything the component does is arithmetic on that
|
|
9
|
+
* path, so it lives here, pure and tested, and `<Madlib>` only projects it.
|
|
10
|
+
*
|
|
11
|
+
* The tree is plain data. `label` is what a node reads as when it is the
|
|
12
|
+
* chosen option; `connector` is the text that follows it before the next
|
|
13
|
+
* blank; the root's `label` is the sentence's leading text. Nothing here
|
|
14
|
+
* knows what a path *means* — that is the host's mapping, on the way out.
|
|
15
|
+
*/
|
|
16
|
+
/** The child of `node` with this id, or undefined — a leaf has none. */
|
|
17
|
+
function childOf(node, id) {
|
|
18
|
+
return node.children?.find((child) => child.id === id);
|
|
19
|
+
}
|
|
20
|
+
/** Whether `node` still has a level below it. */
|
|
21
|
+
function hasChildren(node) {
|
|
22
|
+
return node.children !== undefined && node.children.length > 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a path against the tree, one level per id, stopping at the first
|
|
26
|
+
* id that is not a child of the level above (or at a leaf). The result is what
|
|
27
|
+
* a control per level needs: the chosen node and its siblings.
|
|
28
|
+
*
|
|
29
|
+
* @param tree - The root node
|
|
30
|
+
* @param path - Node ids, root to leaf
|
|
31
|
+
* @returns One entry per resolved level, possibly fewer than `path.length`
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* walkTree(tree, ['jobs', 'bcn'])
|
|
35
|
+
* // → [{ node: jobs, siblings: [jobs, projects] }, { node: bcn, siblings: [bcn, remote] }]
|
|
36
|
+
*/
|
|
37
|
+
export function walkTree(tree, path) {
|
|
38
|
+
const levels = [];
|
|
39
|
+
let current = tree;
|
|
40
|
+
for (const id of path) {
|
|
41
|
+
if (!current.children)
|
|
42
|
+
break;
|
|
43
|
+
const match = childOf(current, id);
|
|
44
|
+
if (!match)
|
|
45
|
+
break;
|
|
46
|
+
levels.push({ node: match, siblings: current.children });
|
|
47
|
+
current = match;
|
|
48
|
+
}
|
|
49
|
+
return levels;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Completes a partial path down to a leaf by taking the first child at every
|
|
53
|
+
* remaining level. The prefix is kept as far as it resolves; from the first
|
|
54
|
+
* id that does not, the defaults take over. `completePath(tree, [])` is the
|
|
55
|
+
* tree's default sentence.
|
|
56
|
+
*
|
|
57
|
+
* @param tree - The root node
|
|
58
|
+
* @param partial - Leading node ids, possibly empty, possibly partly invalid
|
|
59
|
+
* @returns A full root-to-leaf path
|
|
60
|
+
*/
|
|
61
|
+
export function completePath(tree, partial) {
|
|
62
|
+
const path = [];
|
|
63
|
+
let current = tree;
|
|
64
|
+
for (const id of partial) {
|
|
65
|
+
const match = childOf(current, id);
|
|
66
|
+
if (!match)
|
|
67
|
+
break;
|
|
68
|
+
path.push(id);
|
|
69
|
+
current = match;
|
|
70
|
+
}
|
|
71
|
+
while (hasChildren(current)) {
|
|
72
|
+
const first = current.children[0];
|
|
73
|
+
path.push(first.id);
|
|
74
|
+
current = first;
|
|
75
|
+
}
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The tree's default sentence: first child at every level.
|
|
80
|
+
*
|
|
81
|
+
* @param tree - The root node
|
|
82
|
+
* @returns A full root-to-leaf path, empty for a tree with no children
|
|
83
|
+
*/
|
|
84
|
+
export function defaultPath(tree) {
|
|
85
|
+
return completePath(tree, []);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Changes one level of a path and re-completes everything below it.
|
|
89
|
+
*
|
|
90
|
+
* Levels above `depth` are kept. Below it, each level prefers the id the
|
|
91
|
+
* previous path had at that same depth when it is still a valid option there
|
|
92
|
+
* — so switching "maize" to "wheat" keeps "by area" — and falls back to the
|
|
93
|
+
* first child when it is not, which is what happens across a branch change or
|
|
94
|
+
* when the levels shift.
|
|
95
|
+
*
|
|
96
|
+
* @param tree - The root node
|
|
97
|
+
* @param path - The current path
|
|
98
|
+
* @param depth - Index of the level being changed
|
|
99
|
+
* @param id - The new node id at that level
|
|
100
|
+
* @returns A full root-to-leaf path
|
|
101
|
+
*/
|
|
102
|
+
export function replaceAt(tree, path, depth, id) {
|
|
103
|
+
const next = [...path.slice(0, depth), id];
|
|
104
|
+
let current = tree;
|
|
105
|
+
for (const step of next) {
|
|
106
|
+
const match = childOf(current, step);
|
|
107
|
+
if (!match)
|
|
108
|
+
break;
|
|
109
|
+
current = match;
|
|
110
|
+
}
|
|
111
|
+
while (hasChildren(current)) {
|
|
112
|
+
const previous = path[next.length];
|
|
113
|
+
const kept = previous === undefined ? undefined : childOf(current, previous);
|
|
114
|
+
const chosen = kept ?? current.children[0];
|
|
115
|
+
next.push(chosen.id);
|
|
116
|
+
current = chosen;
|
|
117
|
+
}
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Every root-to-leaf path, depth first, in the order the tree declares them.
|
|
122
|
+
* A host uses it to cycle through all sentences, or to check that each one
|
|
123
|
+
* resolves to something.
|
|
124
|
+
*
|
|
125
|
+
* @param tree - The root node
|
|
126
|
+
* @returns All full paths; `[[]]` for a tree with no children
|
|
127
|
+
*/
|
|
128
|
+
export function leafPaths(tree) {
|
|
129
|
+
const paths = [];
|
|
130
|
+
const visit = (node, prefix) => {
|
|
131
|
+
if (!hasChildren(node)) {
|
|
132
|
+
paths.push(prefix);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const child of node.children)
|
|
136
|
+
visit(child, [...prefix, child.id]);
|
|
137
|
+
};
|
|
138
|
+
visit(tree, []);
|
|
139
|
+
return paths;
|
|
140
|
+
}
|
package/dist/madlib.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The madlib: a sentence with blanks, where each blank is a level of a tree
|
|
3
|
+
* and the value is the path of chosen ids. `<Madlib>` renders one; the tree
|
|
4
|
+
* helpers are the arithmetic it is a projection of, exported so a host can
|
|
5
|
+
* complete a deep link, enumerate every sentence, or drive a second control
|
|
6
|
+
* off the same tree. `<InlineSelect>` is the blank on its own, for a lone
|
|
7
|
+
* choice inside a heading.
|
|
8
|
+
*
|
|
9
|
+
* Domain-free: it knows ids, labels and connectors, and nothing about what a
|
|
10
|
+
* path means. Themed with `--vit-madlib-*` custom properties rather than
|
|
11
|
+
* utility classes, and not re-exported from the package root.
|
|
12
|
+
*/
|
|
13
|
+
export { default as Madlib } from './components/madlib/Madlib.svelte';
|
|
14
|
+
export type { MadlibControl, MadlibControls } from './components/madlib/Madlib.svelte';
|
|
15
|
+
export { default as InlineSelect } from './components/madlib/InlineSelect.svelte';
|
|
16
|
+
export type { InlineOption } from './components/madlib/InlineSelect.svelte';
|
|
17
|
+
export { completePath, defaultPath, leafPaths, replaceAt, walkTree, type SentenceLevel, type SentenceNode } from './components/madlib/sentenceTree.js';
|
package/dist/madlib.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The madlib: a sentence with blanks, where each blank is a level of a tree
|
|
3
|
+
* and the value is the path of chosen ids. `<Madlib>` renders one; the tree
|
|
4
|
+
* helpers are the arithmetic it is a projection of, exported so a host can
|
|
5
|
+
* complete a deep link, enumerate every sentence, or drive a second control
|
|
6
|
+
* off the same tree. `<InlineSelect>` is the blank on its own, for a lone
|
|
7
|
+
* choice inside a heading.
|
|
8
|
+
*
|
|
9
|
+
* Domain-free: it knows ids, labels and connectors, and nothing about what a
|
|
10
|
+
* path means. Themed with `--vit-madlib-*` custom properties rather than
|
|
11
|
+
* utility classes, and not re-exported from the package root.
|
|
12
|
+
*/
|
|
13
|
+
export { default as Madlib } from './components/madlib/Madlib.svelte';
|
|
14
|
+
export { default as InlineSelect } from './components/madlib/InlineSelect.svelte';
|
|
15
|
+
export { completePath, defaultPath, leafPaths, replaceAt, walkTree } from './components/madlib/sentenceTree.js';
|
package/dist/styles/tokens.css
CHANGED
|
@@ -92,4 +92,27 @@
|
|
|
92
92
|
--vit-card-shadow: none;
|
|
93
93
|
--vit-card-font: inherit;
|
|
94
94
|
--vit-card-shimmer-color: currentColor;
|
|
95
|
+
|
|
96
|
+
/* Madlib (`./madlib`): a sentence with blanks. The controls inherit the
|
|
97
|
+
font, size and weight of the text around them, so most of these default
|
|
98
|
+
to "whatever the sentence is"; the menu and the accents are the ones a
|
|
99
|
+
theme sets. `--vit-madlib-select-accent` follows `--vit-madlib-accent`
|
|
100
|
+
unless a theme wants the dropdown underline and the toggle underline to
|
|
101
|
+
differ, which the first host did. */
|
|
102
|
+
--vit-madlib-font: inherit;
|
|
103
|
+
--vit-madlib-color: inherit;
|
|
104
|
+
--vit-madlib-weight: inherit;
|
|
105
|
+
--vit-madlib-control-weight: 700;
|
|
106
|
+
--vit-madlib-lead-size: 1em;
|
|
107
|
+
--vit-madlib-lead-line-height: 1.4;
|
|
108
|
+
--vit-madlib-size: 1em;
|
|
109
|
+
--vit-madlib-line-height: 1.4;
|
|
110
|
+
--vit-madlib-muted-color: color-mix(in srgb, currentColor 35%, transparent);
|
|
111
|
+
--vit-madlib-accent: currentColor;
|
|
112
|
+
--vit-madlib-select-accent: var(--vit-madlib-accent);
|
|
113
|
+
--vit-madlib-menu-bg: var(--color-surface);
|
|
114
|
+
--vit-madlib-menu-hover: color-mix(in srgb, currentColor 10%, transparent);
|
|
115
|
+
--vit-madlib-menu-shadow: none;
|
|
116
|
+
--vit-madlib-menu-max-height: 15rem;
|
|
117
|
+
--vit-madlib-menu-z: 40;
|
|
95
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vit-foundation/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -101,6 +101,11 @@
|
|
|
101
101
|
"types": "./dist/overlay.d.ts",
|
|
102
102
|
"svelte": "./dist/overlay.js",
|
|
103
103
|
"default": "./dist/overlay.js"
|
|
104
|
+
},
|
|
105
|
+
"./madlib": {
|
|
106
|
+
"types": "./dist/madlib.d.ts",
|
|
107
|
+
"svelte": "./dist/madlib.js",
|
|
108
|
+
"default": "./dist/madlib.js"
|
|
104
109
|
}
|
|
105
110
|
},
|
|
106
111
|
"peerDependencies": {
|