@urbicon-ui/blocks 6.4.0 → 6.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/dist/components/Guide/Guide.svelte +2 -2
- package/dist/components/Guide/GuideArticle.svelte +3 -2
- package/dist/components/Guide/GuideBeacon.svelte +1 -1
- package/dist/components/Guide/GuideHint.svelte +3 -3
- package/dist/components/Guide/GuideMarker.svelte +1 -1
- package/dist/components/Guide/GuideMention.svelte +2 -2
- package/dist/components/Guide/GuidePanel.svelte +100 -23
- package/dist/components/Guide/GuideRef.svelte +87 -0
- package/dist/components/Guide/GuideRef.svelte.d.ts +4 -0
- package/dist/components/Guide/guide-panel.articles.d.ts +33 -0
- package/dist/components/Guide/guide-panel.articles.js +39 -0
- package/dist/components/Guide/guide-panel.context.d.ts +11 -2
- package/dist/components/Guide/guide.variants.d.ts +35 -0
- package/dist/components/Guide/guide.variants.js +30 -0
- package/dist/components/Guide/index.d.ts +53 -2
- package/dist/components/Guide/index.js +2 -1
- package/dist/i18n/index.d.ts +4 -0
- package/dist/primitives/Badge/badge.variants.js +4 -1
- package/dist/primitives/Button/button.variants.js +7 -1
- package/dist/primitives/Combobox/Combobox.svelte +22 -2
- package/dist/primitives/Dialog/Dialog.svelte +1 -1
- package/dist/primitives/Drawer/Drawer.svelte +1 -1
- package/dist/translations/de.d.ts +2 -0
- package/dist/translations/de.js +3 -1
- package/dist/translations/en.d.ts +2 -0
- package/dist/translations/en.js +3 -1
- package/dist/utils/guide.svelte.d.ts +1 -1
- package/dist/utils/guide.svelte.js +4 -1
- package/package.json +3 -3
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
const guide = getGuideContext();
|
|
34
34
|
|
|
35
|
-
if (import.meta.env
|
|
35
|
+
if (import.meta.env?.DEV && !guide) {
|
|
36
36
|
console.warn(
|
|
37
37
|
'[Guide] <Guide> is used without a <GuideProvider> ancestor — the tour will not render.'
|
|
38
38
|
);
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
// clickable, and only an app-side controller.next() from outside the spotlight can advance.
|
|
218
218
|
// Usually a step-definition mistake; mirrors the unresolved-target warning.
|
|
219
219
|
$effect(() => {
|
|
220
|
-
if (!import.meta.env
|
|
220
|
+
if (!import.meta.env?.DEV || !guide) return;
|
|
221
221
|
if (step?.advance === 'action' && !step.interactive) {
|
|
222
222
|
console.warn(
|
|
223
223
|
`[Guide] tour "${guide.activeTour?.id}" step ${stepIndex}: advance:'action' without ` +
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
let {
|
|
9
9
|
id,
|
|
10
10
|
title,
|
|
11
|
+
group,
|
|
11
12
|
children,
|
|
12
13
|
class: className = '',
|
|
13
14
|
unstyled: unstyledProp = false,
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
|
|
22
23
|
// Register with the parent panel's list view. Runs regardless of whether this
|
|
23
24
|
// article is the active one, so every article shows up in the list. Cleans up
|
|
24
|
-
// on unmount and re-registers if id/title change.
|
|
25
|
+
// on unmount and re-registers if id/title/group change.
|
|
25
26
|
$effect(() => {
|
|
26
|
-
return panel?.registerArticle(id, title);
|
|
27
|
+
return panel?.registerArticle(id, title, group);
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const blocksConfig = getBlocksConfig();
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
const guide = getGuideContext();
|
|
39
39
|
|
|
40
|
-
if (import.meta.env
|
|
40
|
+
if (import.meta.env?.DEV && !guide) {
|
|
41
41
|
console.warn(
|
|
42
42
|
'[Guide] <GuideHint> is used without a <GuideProvider> ancestor — it will not render.'
|
|
43
43
|
);
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
$effect(() => {
|
|
76
76
|
if (trigger === 'manual') {
|
|
77
77
|
if (open) dismissed = false;
|
|
78
|
-
} else if (import.meta.env
|
|
78
|
+
} else if (import.meta.env?.DEV && open) {
|
|
79
79
|
console.warn('[Guide] <GuideHint open> is ignored unless trigger="manual".');
|
|
80
80
|
}
|
|
81
81
|
});
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
// DEV resilience: a requested hint whose target can't be resolved is a silent
|
|
180
180
|
// no-op otherwise — warn so a wrong `for` id is noticed (mirrors the engine).
|
|
181
181
|
$effect(() => {
|
|
182
|
-
if (import.meta.env
|
|
182
|
+
if (import.meta.env?.DEV && guide && visible && !guide.resolveTarget(topicId)) {
|
|
183
183
|
console.warn(
|
|
184
184
|
`[Guide] <GuideHint for="${topicId}"> cannot anchor — no element with [data-guide="${topicId}"] (or registered target) found.`
|
|
185
185
|
);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const guide = getGuideContext();
|
|
20
20
|
|
|
21
|
-
if (import.meta.env
|
|
21
|
+
if (import.meta.env?.DEV && !guide) {
|
|
22
22
|
console.warn(
|
|
23
23
|
'[Guide] <GuideMention> is used without a <GuideProvider> ancestor — it renders as plain text.'
|
|
24
24
|
);
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
const interactive = $derived(!!guide && effectiveDirection !== 'to-guide');
|
|
43
43
|
|
|
44
44
|
$effect(() => {
|
|
45
|
-
if (import.meta.env
|
|
45
|
+
if (import.meta.env?.DEV && guide && effectiveDirection === 'to-guide') {
|
|
46
46
|
console.warn(
|
|
47
47
|
`[Guide] <GuideMention for="${topicId}"> points at a topic whose direction excludes Guide→UI ('to-guide') — it renders as plain text.`
|
|
48
48
|
);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
7
7
|
import { getGuideContext } from './guide.context';
|
|
8
8
|
import { setGuidePanelContext } from './guide-panel.context';
|
|
9
|
+
import { filterArticles, groupArticles, hasNamedGroups } from './guide-panel.articles';
|
|
9
10
|
import { guidePanelVariants, type GuidePanelVariants } from './guide.variants';
|
|
10
11
|
import type { GuidePanelProps } from './index';
|
|
11
12
|
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
placement = 'right',
|
|
17
18
|
size = 'md',
|
|
18
19
|
title,
|
|
20
|
+
searchable = false,
|
|
19
21
|
closeOnEscape = true,
|
|
20
22
|
footer,
|
|
21
23
|
children,
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
// Re-registers if the id changes; releases it on unmount.
|
|
36
38
|
$effect(() => guide?.registerPanel(panelId));
|
|
37
39
|
|
|
38
|
-
if (import.meta.env
|
|
40
|
+
if (import.meta.env?.DEV && !guide) {
|
|
39
41
|
console.warn(
|
|
40
42
|
'[Guide] <GuidePanel> is used without a <GuideProvider> ancestor — it will not render.'
|
|
41
43
|
);
|
|
@@ -44,12 +46,16 @@
|
|
|
44
46
|
// Article registry feeding the list view. GuideArticle children register here.
|
|
45
47
|
// untrack mirrors GuideController.registerTarget: writes happen from a child's
|
|
46
48
|
// $effect, so untracking guards the list derived against effect_update_depth_exceeded.
|
|
47
|
-
|
|
49
|
+
// SvelteMap preserves insertion order, so the index follows definition order.
|
|
50
|
+
const articleMap = new SvelteMap<string, { title: string; group?: string }>();
|
|
48
51
|
setGuidePanelContext({
|
|
49
|
-
registerArticle(id, articleTitle) {
|
|
50
|
-
untrack(() => articleMap.set(id, articleTitle));
|
|
52
|
+
registerArticle(id, articleTitle, group) {
|
|
53
|
+
untrack(() => articleMap.set(id, { title: articleTitle, group }));
|
|
51
54
|
return () => untrack(() => articleMap.delete(id));
|
|
52
|
-
}
|
|
55
|
+
},
|
|
56
|
+
// Tracked read (no untrack): a GuideRef's reactive resolvable re-runs as
|
|
57
|
+
// articles register/unregister.
|
|
58
|
+
hasArticle: (id) => articleMap.has(id)
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
const blocksConfig = getBlocksConfig();
|
|
@@ -62,11 +68,28 @@
|
|
|
62
68
|
|
|
63
69
|
const open = $derived(guide?.panelOpen ?? false);
|
|
64
70
|
const activeArticle = $derived(guide?.activeArticle ?? null);
|
|
65
|
-
const articles = $derived(
|
|
71
|
+
const articles = $derived(
|
|
72
|
+
// Normalize a blank/whitespace `group` to `undefined` so it collapses into the
|
|
73
|
+
// ungrouped block instead of rendering an empty (padded) section header.
|
|
74
|
+
[...articleMap].map(([id, a]) => ({ id, title: a.title, group: a.group?.trim() || undefined }))
|
|
75
|
+
);
|
|
66
76
|
const headerTitle = $derived(
|
|
67
|
-
(activeArticle ? articleMap.get(activeArticle) : undefined) ??
|
|
77
|
+
(activeArticle ? articleMap.get(activeArticle)?.title : undefined) ??
|
|
78
|
+
title ??
|
|
79
|
+
bt('guide.openHelp', {})
|
|
68
80
|
);
|
|
69
81
|
|
|
82
|
+
// Optional title filter (#26). Grouping below runs on the filtered set, so a
|
|
83
|
+
// search narrows the index while keeping non-empty sections' headers.
|
|
84
|
+
let searchQuery = $state('');
|
|
85
|
+
const filteredArticles = $derived(searchable ? filterArticles(articles, searchQuery) : articles);
|
|
86
|
+
const isFiltering = $derived(searchable && searchQuery.trim().length > 0);
|
|
87
|
+
|
|
88
|
+
// Group the index into sections (first-occurrence order; ungrouped articles in
|
|
89
|
+
// one headerless block). When no article sets a group, this is a flat list.
|
|
90
|
+
const sections = $derived(groupArticles(filteredArticles));
|
|
91
|
+
const hasGroups = $derived(hasNamedGroups(sections));
|
|
92
|
+
|
|
70
93
|
const panelTransform = $derived(
|
|
71
94
|
open ? 'translateX(0)' : placement === 'left' ? 'translateX(-100%)' : 'translateX(100%)'
|
|
72
95
|
);
|
|
@@ -88,7 +111,7 @@
|
|
|
88
111
|
// shows an empty body with no signal. Deferred past the current flush via `tick()` so a
|
|
89
112
|
// sibling <GuideArticle> registering in the same render isn't a false positive.
|
|
90
113
|
$effect(() => {
|
|
91
|
-
if (!import.meta.env
|
|
114
|
+
if (!import.meta.env?.DEV) return;
|
|
92
115
|
const id = activeArticle;
|
|
93
116
|
if (!id) return;
|
|
94
117
|
let cancelled = false;
|
|
@@ -180,25 +203,79 @@
|
|
|
180
203
|
</header>
|
|
181
204
|
|
|
182
205
|
<div class={unstyled ? (slotClasses?.body ?? '') : styles.body({ class: slotClasses?.body })}>
|
|
206
|
+
{#snippet articleListItem(article: { id: string; title: string })}
|
|
207
|
+
<li>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
class={unstyled
|
|
211
|
+
? (slotClasses?.listItem ?? '')
|
|
212
|
+
: styles.listItem({ class: slotClasses?.listItem })}
|
|
213
|
+
onclick={() => guide?.setArticle(article.id)}
|
|
214
|
+
>
|
|
215
|
+
<span class="flex-1">{article.title}</span>
|
|
216
|
+
<ChevronRightIcon class="h-4 w-4 opacity-50" />
|
|
217
|
+
</button>
|
|
218
|
+
</li>
|
|
219
|
+
{/snippet}
|
|
183
220
|
{#if !activeArticle}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
{#if searchable}
|
|
222
|
+
<input
|
|
223
|
+
type="search"
|
|
224
|
+
class={unstyled
|
|
225
|
+
? (slotClasses?.searchInput ?? '')
|
|
226
|
+
: styles.searchInput({ class: slotClasses?.searchInput })}
|
|
227
|
+
placeholder={bt('guide.filterPlaceholder', {})}
|
|
228
|
+
aria-label={bt('guide.filterPlaceholder', {})}
|
|
229
|
+
bind:value={searchQuery}
|
|
230
|
+
/>
|
|
231
|
+
<!-- Persistent polite announcer: present in the a11y tree before the result
|
|
232
|
+
set empties, so a filter narrowing to zero is reliably announced (a region
|
|
233
|
+
inserted together with its text is dropped by some screen readers). The
|
|
234
|
+
visible empty-state below carries no role, to avoid a duplicate region. -->
|
|
235
|
+
<p aria-live="polite" class="sr-only">
|
|
236
|
+
{#if isFiltering && filteredArticles.length === 0}{bt('guide.noResults', {})}{/if}
|
|
237
|
+
</p>
|
|
238
|
+
{/if}
|
|
239
|
+
{#if isFiltering && filteredArticles.length === 0}
|
|
240
|
+
<p
|
|
241
|
+
class={unstyled
|
|
242
|
+
? (slotClasses?.noResults ?? '')
|
|
243
|
+
: styles.noResults({ class: slotClasses?.noResults })}
|
|
244
|
+
>
|
|
245
|
+
{bt('guide.noResults', {})}
|
|
246
|
+
</p>
|
|
247
|
+
{:else if hasGroups}
|
|
248
|
+
{#each sections as section, i (section.group ?? '')}
|
|
249
|
+
{#if section.group}
|
|
250
|
+
<h3
|
|
251
|
+
id={`${panelId}-group-${i}`}
|
|
191
252
|
class={unstyled
|
|
192
|
-
? (slotClasses?.
|
|
193
|
-
: styles.
|
|
194
|
-
onclick={() => guide.setArticle(article.id)}
|
|
253
|
+
? (slotClasses?.groupHeader ?? '')
|
|
254
|
+
: styles.groupHeader({ class: slotClasses?.groupHeader })}
|
|
195
255
|
>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
256
|
+
{section.group}
|
|
257
|
+
</h3>
|
|
258
|
+
{/if}
|
|
259
|
+
<ul
|
|
260
|
+
class={unstyled
|
|
261
|
+
? (slotClasses?.list ?? '')
|
|
262
|
+
: styles.list({ class: slotClasses?.list })}
|
|
263
|
+
aria-labelledby={section.group ? `${panelId}-group-${i}` : undefined}
|
|
264
|
+
>
|
|
265
|
+
{#each section.articles as article (article.id)}
|
|
266
|
+
{@render articleListItem(article)}
|
|
267
|
+
{/each}
|
|
268
|
+
</ul>
|
|
200
269
|
{/each}
|
|
201
|
-
|
|
270
|
+
{:else}
|
|
271
|
+
<ul
|
|
272
|
+
class={unstyled ? (slotClasses?.list ?? '') : styles.list({ class: slotClasses?.list })}
|
|
273
|
+
>
|
|
274
|
+
{#each filteredArticles as article (article.id)}
|
|
275
|
+
{@render articleListItem(article)}
|
|
276
|
+
{/each}
|
|
277
|
+
</ul>
|
|
278
|
+
{/if}
|
|
202
279
|
{/if}
|
|
203
280
|
{@render children?.()}
|
|
204
281
|
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
|
+
import { getGuideContext } from './guide.context';
|
|
5
|
+
import { getGuidePanelContext } from './guide-panel.context';
|
|
6
|
+
import { guideRefVariants, type GuideRefVariants } from './guide.variants';
|
|
7
|
+
import type { GuideRefProps } from './index';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
article,
|
|
11
|
+
children,
|
|
12
|
+
class: className = '',
|
|
13
|
+
unstyled: unstyledProp = false,
|
|
14
|
+
slotClasses: slotClassesProp = {},
|
|
15
|
+
preset,
|
|
16
|
+
...restProps
|
|
17
|
+
}: GuideRefProps = $props();
|
|
18
|
+
|
|
19
|
+
const guide = getGuideContext();
|
|
20
|
+
const panel = getGuidePanelContext();
|
|
21
|
+
|
|
22
|
+
if (import.meta.env?.DEV && !guide) {
|
|
23
|
+
console.warn(
|
|
24
|
+
'[Guide] <GuideRef> is used without a <GuideProvider> ancestor — it renders as plain text.'
|
|
25
|
+
);
|
|
26
|
+
} else if (import.meta.env?.DEV && !panel) {
|
|
27
|
+
console.warn('[Guide] <GuideRef> is used outside a <GuidePanel> — it renders as plain text.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const blocksConfig = getBlocksConfig();
|
|
31
|
+
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
32
|
+
const variantProps: GuideRefVariants = $derived({});
|
|
33
|
+
const styles = $derived(guideRefVariants(variantProps));
|
|
34
|
+
const slotClasses = $derived(
|
|
35
|
+
resolveSlotClasses(blocksConfig, 'GuideRef', preset, variantProps, slotClassesProp)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Interactive only when wired to a panel that actually registers the target
|
|
39
|
+
// article; otherwise (no provider, outside a panel, or an unknown id) it degrades
|
|
40
|
+
// to plain inline text — mirrors GuideMention's no-provider fallback. Articles
|
|
41
|
+
// register from a child `$effect`, so `hasArticle` is false during SSR *and* the
|
|
42
|
+
// first client render → both emit a `<span>` (no hydration mismatch); a known
|
|
43
|
+
// article reactively upgrades to a `<button>` once its registration effect runs.
|
|
44
|
+
const resolvable = $derived(!!panel?.hasArticle(article));
|
|
45
|
+
const interactive = $derived(!!guide && resolvable);
|
|
46
|
+
|
|
47
|
+
// DEV-only: warn for an article id that stays unresolved past this flush — deferred
|
|
48
|
+
// via `tick()` so a target `<GuideArticle>` registering in the same render isn't a
|
|
49
|
+
// false positive (mirrors GuidePanel's missing-active-article warning).
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (!import.meta.env?.DEV || !guide || !panel) return;
|
|
52
|
+
const target = article;
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
tick().then(() => {
|
|
55
|
+
if (!cancelled && !panel.hasArticle(target)) {
|
|
56
|
+
console.warn(
|
|
57
|
+
`[Guide] <GuideRef article="${target}"> points at an article that is not registered — it renders as plain text.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function navigate() {
|
|
67
|
+
guide?.setArticle(article);
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
{#if interactive}
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class={unstyled
|
|
75
|
+
? [slotClasses?.ref, className].filter(Boolean).join(' ')
|
|
76
|
+
: styles.ref({ class: [slotClasses?.ref, className] })}
|
|
77
|
+
data-guide-ref
|
|
78
|
+
onclick={navigate}
|
|
79
|
+
{...restProps}
|
|
80
|
+
>
|
|
81
|
+
{@render children?.()}
|
|
82
|
+
</button>
|
|
83
|
+
{:else}
|
|
84
|
+
<span class={[slotClasses?.ref, className].filter(Boolean).join(' ') || undefined} {...restProps}>
|
|
85
|
+
{@render children?.()}
|
|
86
|
+
</span>
|
|
87
|
+
{/if}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transforms for the `GuidePanel` article index — grouping (#25) and
|
|
3
|
+
* filtering (#26). Kept framework-free so the index logic is unit-testable
|
|
4
|
+
* without rendering the panel (mirrors `pagination.engine` / `planner.bucket`).
|
|
5
|
+
*/
|
|
6
|
+
/** A registered article as surfaced to the panel index. */
|
|
7
|
+
export interface IndexArticle {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
/** Optional section label; `undefined` means the ungrouped block. */
|
|
11
|
+
group?: string;
|
|
12
|
+
}
|
|
13
|
+
/** One section of the grouped index. */
|
|
14
|
+
export interface ArticleSection {
|
|
15
|
+
/** The section's group label, or `undefined` for the headerless ungrouped block. */
|
|
16
|
+
group: string | undefined;
|
|
17
|
+
articles: IndexArticle[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Group articles into sections keyed by `group`, in first-occurrence order over
|
|
21
|
+
* the (insertion-ordered) input. Articles without a `group` collect into one
|
|
22
|
+
* headerless section, positioned where its first ungrouped article appears.
|
|
23
|
+
* When no article sets a `group`, the result is a single ungrouped section —
|
|
24
|
+
* i.e. a flat list.
|
|
25
|
+
*/
|
|
26
|
+
export declare function groupArticles(articles: IndexArticle[]): ArticleSection[];
|
|
27
|
+
/** Whether any section carries a real group label (drives grouped vs. flat rendering). */
|
|
28
|
+
export declare function hasNamedGroups(sections: ArticleSection[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Filter articles by a case-insensitive substring match on the title. A blank
|
|
31
|
+
* query returns the input unchanged. Whitespace around the query is ignored.
|
|
32
|
+
*/
|
|
33
|
+
export declare function filterArticles(articles: IndexArticle[], query: string): IndexArticle[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transforms for the `GuidePanel` article index — grouping (#25) and
|
|
3
|
+
* filtering (#26). Kept framework-free so the index logic is unit-testable
|
|
4
|
+
* without rendering the panel (mirrors `pagination.engine` / `planner.bucket`).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Group articles into sections keyed by `group`, in first-occurrence order over
|
|
8
|
+
* the (insertion-ordered) input. Articles without a `group` collect into one
|
|
9
|
+
* headerless section, positioned where its first ungrouped article appears.
|
|
10
|
+
* When no article sets a `group`, the result is a single ungrouped section —
|
|
11
|
+
* i.e. a flat list.
|
|
12
|
+
*/
|
|
13
|
+
export function groupArticles(articles) {
|
|
14
|
+
const byGroup = new Map();
|
|
15
|
+
for (const article of articles) {
|
|
16
|
+
const key = article.group ?? '';
|
|
17
|
+
let section = byGroup.get(key);
|
|
18
|
+
if (!section) {
|
|
19
|
+
section = { group: article.group, articles: [] };
|
|
20
|
+
byGroup.set(key, section);
|
|
21
|
+
}
|
|
22
|
+
section.articles.push(article);
|
|
23
|
+
}
|
|
24
|
+
return [...byGroup.values()];
|
|
25
|
+
}
|
|
26
|
+
/** Whether any section carries a real group label (drives grouped vs. flat rendering). */
|
|
27
|
+
export function hasNamedGroups(sections) {
|
|
28
|
+
return sections.some((s) => s.group !== undefined);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Filter articles by a case-insensitive substring match on the title. A blank
|
|
32
|
+
* query returns the input unchanged. Whitespace around the query is ignored.
|
|
33
|
+
*/
|
|
34
|
+
export function filterArticles(articles, query) {
|
|
35
|
+
const q = query.trim().toLowerCase();
|
|
36
|
+
if (!q)
|
|
37
|
+
return articles;
|
|
38
|
+
return articles.filter((a) => a.title.toLowerCase().includes(q));
|
|
39
|
+
}
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* outside a `GuidePanel` simply renders inert.
|
|
5
5
|
*/
|
|
6
6
|
export interface GuidePanelContext {
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Register an article (by title, with an optional `group` for the index's
|
|
9
|
+
* section headers) and return an unregister cleanup.
|
|
10
|
+
*/
|
|
11
|
+
registerArticle(id: string, title: string, group?: string): () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Whether an article with this id is currently registered. Reactive — lets a
|
|
14
|
+
* `GuideRef` degrade to plain text for an unknown article. Reads the registry
|
|
15
|
+
* untracked-free so callers re-run when articles register/unregister.
|
|
16
|
+
*/
|
|
17
|
+
hasArticle(id: string): boolean;
|
|
9
18
|
}
|
|
10
19
|
declare const getGuidePanelContext: () => GuidePanelContext | undefined, setGuidePanelContext: (value: GuidePanelContext | undefined) => GuidePanelContext | undefined;
|
|
11
20
|
export { getGuidePanelContext, setGuidePanelContext };
|
|
@@ -68,6 +68,27 @@ export declare const guidePanelVariants: (props?: import("../../utils/variants.j
|
|
|
68
68
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
69
69
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
70
70
|
}) | undefined) => string;
|
|
71
|
+
searchInput: (props?: ({
|
|
72
|
+
placement?: "right" | "left" | undefined;
|
|
73
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
74
|
+
} & {
|
|
75
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
76
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
77
|
+
}) | undefined) => string;
|
|
78
|
+
noResults: (props?: ({
|
|
79
|
+
placement?: "right" | "left" | undefined;
|
|
80
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
81
|
+
} & {
|
|
82
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
83
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
84
|
+
}) | undefined) => string;
|
|
85
|
+
groupHeader: (props?: ({
|
|
86
|
+
placement?: "right" | "left" | undefined;
|
|
87
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
88
|
+
} & {
|
|
89
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
90
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
91
|
+
}) | undefined) => string;
|
|
71
92
|
list: (props?: ({
|
|
72
93
|
placement?: "right" | "left" | undefined;
|
|
73
94
|
size?: "sm" | "md" | "lg" | undefined;
|
|
@@ -140,6 +161,18 @@ export declare const guideMentionVariants: (props?: import("../../utils/variants
|
|
|
140
161
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
141
162
|
}) | undefined) => string;
|
|
142
163
|
};
|
|
164
|
+
/**
|
|
165
|
+
* Styling for `GuideRef` — an inline article→article link inside article prose.
|
|
166
|
+
* Mirrors `GuideMention`'s link-like reset, but with a *solid* underline (vs the
|
|
167
|
+
* Mention's dotted one) that thickens on hover/focus, so a panel-internal jump
|
|
168
|
+
* reads distinctly from a Mention's UI highlight.
|
|
169
|
+
*/
|
|
170
|
+
export declare const guideRefVariants: (props?: import("../../utils/variants.js").TVProps<{}> | undefined) => {
|
|
171
|
+
ref: (props?: ({} & {
|
|
172
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
173
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
174
|
+
}) | undefined) => string;
|
|
175
|
+
};
|
|
143
176
|
/**
|
|
144
177
|
* Styling for `GuideHint` — a small, non-blocking bubble anchored to a
|
|
145
178
|
* `data-guide` element via `floating.ts`, rendered in the native popover
|
|
@@ -275,6 +308,7 @@ export type GuideBeaconVariants = VariantProps<typeof guideBeaconVariants>;
|
|
|
275
308
|
export type GuideArticleVariants = VariantProps<typeof guideArticleVariants>;
|
|
276
309
|
export type GuideMarkerVariants = VariantProps<typeof guideMarkerVariants>;
|
|
277
310
|
export type GuideMentionVariants = VariantProps<typeof guideMentionVariants>;
|
|
311
|
+
export type GuideRefVariants = VariantProps<typeof guideRefVariants>;
|
|
278
312
|
export type GuideHintVariants = VariantProps<typeof guideHintVariants>;
|
|
279
313
|
/** Slot names derived from each `tv()` config — single source of truth for `slotClasses`. */
|
|
280
314
|
export type GuidePanelSlots = SlotNames<typeof guidePanelVariants>;
|
|
@@ -283,4 +317,5 @@ export type GuideBeaconSlots = SlotNames<typeof guideBeaconVariants>;
|
|
|
283
317
|
export type GuideArticleSlots = SlotNames<typeof guideArticleVariants>;
|
|
284
318
|
export type GuideMarkerSlots = SlotNames<typeof guideMarkerVariants>;
|
|
285
319
|
export type GuideMentionSlots = SlotNames<typeof guideMentionVariants>;
|
|
320
|
+
export type GuideRefSlots = SlotNames<typeof guideRefVariants>;
|
|
286
321
|
export type GuideHintSlots = SlotNames<typeof guideHintVariants>;
|
|
@@ -25,6 +25,19 @@ export const guidePanelVariants = tv({
|
|
|
25
25
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary/50'
|
|
26
26
|
],
|
|
27
27
|
body: ['flex-1 overflow-y-auto overscroll-contain px-5 py-4'],
|
|
28
|
+
// Filter input above the index (opt-in via GuidePanel `searchable`).
|
|
29
|
+
searchInput: [
|
|
30
|
+
'mb-3 w-full rounded-lg border border-border-hairline bg-surface-base px-3 py-2 text-sm',
|
|
31
|
+
'text-text-primary placeholder:text-text-tertiary',
|
|
32
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary/50'
|
|
33
|
+
],
|
|
34
|
+
// Empty-state shown when a search filters every article out.
|
|
35
|
+
noResults: ['px-3 py-6 text-center text-sm text-text-tertiary'],
|
|
36
|
+
// Section label above a group of articles in the index (opt-in via GuideArticle `group`).
|
|
37
|
+
groupHeader: [
|
|
38
|
+
'px-3 pb-1 pt-4 text-xs font-semibold uppercase tracking-wide text-text-tertiary',
|
|
39
|
+
'first:pt-1'
|
|
40
|
+
],
|
|
28
41
|
list: ['flex flex-col gap-1'],
|
|
29
42
|
listItem: [
|
|
30
43
|
'flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm transition-colors',
|
|
@@ -107,6 +120,23 @@ export const guideMentionVariants = tv({
|
|
|
107
120
|
]
|
|
108
121
|
}
|
|
109
122
|
});
|
|
123
|
+
/**
|
|
124
|
+
* Styling for `GuideRef` — an inline article→article link inside article prose.
|
|
125
|
+
* Mirrors `GuideMention`'s link-like reset, but with a *solid* underline (vs the
|
|
126
|
+
* Mention's dotted one) that thickens on hover/focus, so a panel-internal jump
|
|
127
|
+
* reads distinctly from a Mention's UI highlight.
|
|
128
|
+
*/
|
|
129
|
+
export const guideRefVariants = tv({
|
|
130
|
+
slots: {
|
|
131
|
+
ref: [
|
|
132
|
+
'm-0 inline cursor-pointer border-0 bg-transparent p-0 text-left font-medium text-primary',
|
|
133
|
+
'underline decoration-solid decoration-1 underline-offset-2',
|
|
134
|
+
'transition-[text-decoration-thickness] duration-[var(--blocks-duration-fast)]',
|
|
135
|
+
'hover:decoration-2 focus-visible:decoration-2',
|
|
136
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary/50 focus-visible:rounded-[0.2em]'
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
});
|
|
110
140
|
/**
|
|
111
141
|
* Styling for `GuideHint` — a small, non-blocking bubble anchored to a
|
|
112
142
|
* `data-guide` element via `floating.ts`, rendered in the native popover
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { GuideController, GuideDirection, GuideStorageAdapter, GuideTour, Placement } from '../../utils/index.js';
|
|
3
|
-
import type { GuideArticleSlots, GuideBeaconSlots, GuideBeaconVariants, GuideHintSlots, GuideMarkerSlots, GuideMarkerVariants, GuideMentionSlots, GuidePanelSlots, GuidePanelVariants, GuideTourSlots } from './guide.variants.js';
|
|
3
|
+
import type { GuideArticleSlots, GuideBeaconSlots, GuideBeaconVariants, GuideHintSlots, GuideMarkerSlots, GuideMarkerVariants, GuideMentionSlots, GuidePanelSlots, GuidePanelVariants, GuideRefSlots, GuideTourSlots } from './guide.variants.js';
|
|
4
4
|
/**
|
|
5
5
|
* @description Root provider for the Guide help system. Instantiates a `GuideController`
|
|
6
6
|
* and shares it via context with all Guide surfaces (Panel, Marker, Mention, Hint, Tour).
|
|
@@ -74,6 +74,13 @@ export interface GuidePanelProps {
|
|
|
74
74
|
size?: GuidePanelVariants['size'];
|
|
75
75
|
/** Heading shown when no article is open. @default i18n `guide.openHelp` */
|
|
76
76
|
title?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Render a filter input above the article index that matches article titles
|
|
79
|
+
* (case-insensitive). Off by default. Pairs with article grouping — filtered
|
|
80
|
+
* results keep their section headers and empty sections disappear.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
searchable?: boolean;
|
|
77
84
|
/** Close the panel when Escape is pressed. @default true */
|
|
78
85
|
closeOnEscape?: boolean;
|
|
79
86
|
/** Optional footer content. */
|
|
@@ -111,6 +118,14 @@ export interface GuideArticleProps {
|
|
|
111
118
|
id: string;
|
|
112
119
|
/** Title shown in the panel list and header. */
|
|
113
120
|
title: string;
|
|
121
|
+
/**
|
|
122
|
+
* Optional section this article belongs to in the panel index. Articles that
|
|
123
|
+
* share a `group` are rendered under one section header (sections appear in
|
|
124
|
+
* the order their first article is defined); articles without a `group` stay
|
|
125
|
+
* in an ungrouped block. When no article sets a `group`, the index is a flat
|
|
126
|
+
* list — unchanged from today.
|
|
127
|
+
*/
|
|
128
|
+
group?: string;
|
|
114
129
|
/** Article body. */
|
|
115
130
|
children?: Snippet;
|
|
116
131
|
/** Additional classes on the article root. */
|
|
@@ -216,6 +231,41 @@ export interface GuideMentionProps {
|
|
|
216
231
|
/** Apply a named preset registered via `<BlocksProvider presets={{ GuideMention: {...} }}>`. */
|
|
217
232
|
preset?: string;
|
|
218
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* @description An inline article→article link inside a `GuideArticle` body — the
|
|
236
|
+
* help-internal analogue of `GuideMention` (which links out to a UI element). A real
|
|
237
|
+
* `<button>` that navigates the open `GuidePanel` to the target article via the
|
|
238
|
+
* controller (`setArticle`). Degrades to plain inline text without a `GuideProvider`,
|
|
239
|
+
* outside a `GuidePanel`, or for an unknown `article` id; keyboard/focus parity with
|
|
240
|
+
* `GuideMention`.
|
|
241
|
+
* @tag navigation
|
|
242
|
+
* @related GuideArticle
|
|
243
|
+
* @related GuidePanel
|
|
244
|
+
* @related GuideMention
|
|
245
|
+
* @stability beta
|
|
246
|
+
* @standalone
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```svelte
|
|
250
|
+
* <GuideArticle id="cost-pot" title="Cost pot">
|
|
251
|
+
* <p>The pot is split by the <GuideRef article="splitting">splitting method</GuideRef>.</p>
|
|
252
|
+
* </GuideArticle>
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export interface GuideRefProps {
|
|
256
|
+
/** Id of the `GuideArticle` to navigate to. Inert (plain text) for an unknown id. */
|
|
257
|
+
article: string;
|
|
258
|
+
/** The link text. */
|
|
259
|
+
children?: Snippet;
|
|
260
|
+
/** Additional classes on the ref. */
|
|
261
|
+
class?: string;
|
|
262
|
+
/** Strip all default styles. @default false */
|
|
263
|
+
unstyled?: boolean;
|
|
264
|
+
/** Per-slot class overrides. */
|
|
265
|
+
slotClasses?: Partial<Record<GuideRefSlots, string>>;
|
|
266
|
+
/** Apply a named preset registered via `<BlocksProvider presets={{ GuideRef: {...} }}>`. */
|
|
267
|
+
preset?: string;
|
|
268
|
+
}
|
|
219
269
|
/**
|
|
220
270
|
* @description A contextual, non-blocking hint anchored to a `data-guide` element via
|
|
221
271
|
* `floating.ts` (flip/shift/arrow), rendered in the native popover top-layer. Waits at the
|
|
@@ -409,4 +459,5 @@ export { default as GuideMarker } from './GuideMarker.svelte';
|
|
|
409
459
|
export { default as GuideMention } from './GuideMention.svelte';
|
|
410
460
|
export { default as GuidePanel } from './GuidePanel.svelte';
|
|
411
461
|
export { default as GuideProvider } from './GuideProvider.svelte';
|
|
412
|
-
export {
|
|
462
|
+
export { default as GuideRef } from './GuideRef.svelte';
|
|
463
|
+
export { type GuideArticleVariants, type GuideBeaconVariants, type GuideHintVariants, type GuideMarkerVariants, type GuideMentionVariants, type GuidePanelVariants, type GuideRefVariants, type GuideTourVariants, guideArticleVariants, guideBeaconVariants, guideHintVariants, guideMarkerVariants, guideMentionVariants, guidePanelVariants, guideRefVariants, guideTourVariants } from './guide.variants.js';
|
|
@@ -6,7 +6,8 @@ export { default as GuideMarker } from './GuideMarker.svelte';
|
|
|
6
6
|
export { default as GuideMention } from './GuideMention.svelte';
|
|
7
7
|
export { default as GuidePanel } from './GuidePanel.svelte';
|
|
8
8
|
export { default as GuideProvider } from './GuideProvider.svelte';
|
|
9
|
-
export {
|
|
9
|
+
export { default as GuideRef } from './GuideRef.svelte';
|
|
10
|
+
export { guideArticleVariants, guideBeaconVariants, guideHintVariants, guideMarkerVariants, guideMentionVariants, guidePanelVariants, guideRefVariants, guideTourVariants } from './guide.variants.js';
|
|
10
11
|
// Note: getGuideContext/setGuideContext are intentionally NOT re-exported here.
|
|
11
12
|
// They are an internal wiring seam — surfaces import them directly from
|
|
12
13
|
// './guide.context' (mirrors Tab/Icon, which keep their context helpers private).
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -128,6 +128,8 @@ declare const blocksTranslations: {
|
|
|
128
128
|
readonly dismiss: "Dismiss hint";
|
|
129
129
|
readonly startTour: "Start the guided tour";
|
|
130
130
|
readonly actionRequired: "Complete the highlighted action to continue";
|
|
131
|
+
readonly filterPlaceholder: "Filter topics…";
|
|
132
|
+
readonly noResults: "No matching topics";
|
|
131
133
|
};
|
|
132
134
|
readonly datepicker: {
|
|
133
135
|
readonly placeholder: "Select a date...";
|
|
@@ -297,6 +299,8 @@ declare const blocksTranslations: {
|
|
|
297
299
|
readonly dismiss: "Hinweis ausblenden";
|
|
298
300
|
readonly startTour: "Geführte Tour starten";
|
|
299
301
|
readonly actionRequired: "Führe die markierte Aktion aus, um fortzufahren";
|
|
302
|
+
readonly filterPlaceholder: "Themen filtern…";
|
|
303
|
+
readonly noResults: "Keine passenden Themen";
|
|
300
304
|
};
|
|
301
305
|
readonly datepicker: {
|
|
302
306
|
readonly placeholder: "Datum wählen...";
|
|
@@ -18,7 +18,10 @@ export const badgeVariants = tv({
|
|
|
18
18
|
'transition-[color,background-color,border-color,box-shadow,opacity] duration-[var(--blocks-duration-fast)] ease-out'
|
|
19
19
|
// Radius driven by `tier` axis below.
|
|
20
20
|
],
|
|
21
|
-
|
|
21
|
+
// `[gap:inherit]` (arbitrary property), NOT `gap-inherit` — Tailwind v4 emits
|
|
22
|
+
// no `gap-inherit` rule, so the icon↔label gap would collapse to 0. The
|
|
23
|
+
// arbitrary property inherits `base`'s per-size gap. (Codeberg #21)
|
|
24
|
+
content: ['flex items-center [gap:inherit]'],
|
|
22
25
|
// tier: modify — small remove-control on a commit-tier badge.
|
|
23
26
|
removeButton: [
|
|
24
27
|
'ml-1 shrink-0 rounded-modify transition-colors text-current',
|
|
@@ -10,7 +10,13 @@ export const buttonVariants = tv({
|
|
|
10
10
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none'
|
|
11
11
|
],
|
|
12
12
|
content: [
|
|
13
|
-
|
|
13
|
+
// `[gap:inherit]` (arbitrary property), NOT `gap-inherit`: Tailwind v4 has
|
|
14
|
+
// no `gap-inherit` utility — the spacing-based `gap-*` scale has no
|
|
15
|
+
// `inherit` member (unlike `text-inherit`/`border-inherit`), so that class
|
|
16
|
+
// emits no rule and the icon↔label gap silently collapses to 0. The
|
|
17
|
+
// arbitrary property makes `content` inherit `base`'s per-size gap as
|
|
18
|
+
// intended. (Codeberg #21)
|
|
19
|
+
'flex items-center [gap:inherit] transition-opacity duration-[var(--blocks-duration-fast)]'
|
|
14
20
|
],
|
|
15
21
|
spinner: [
|
|
16
22
|
'flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-[var(--blocks-duration-fast)]'
|
|
@@ -104,6 +104,25 @@
|
|
|
104
104
|
// Focus never leaves the `<input>` while open — the active option is
|
|
105
105
|
// surfaced via `aria-activedescendant`, not by moving DOM focus into
|
|
106
106
|
// the listbox.
|
|
107
|
+
//
|
|
108
|
+
// The focus restore in `select`/`clear` must NOT re-open the listbox the way
|
|
109
|
+
// a user-initiated focus does. `inputEl.focus()` dispatches `focus`
|
|
110
|
+
// synchronously, so without this guard `handleFocus` would flip `open = true`
|
|
111
|
+
// again on the very next line — negating the `open = false` above and leaving
|
|
112
|
+
// the listbox open after every mouse-click selection/clear (Codeberg #19).
|
|
113
|
+
// The flag is raised immediately before `focus()` and lowered immediately
|
|
114
|
+
// after, so it masks only that one synchronous event; when `focus()` is a
|
|
115
|
+
// no-op (input already focused, e.g. an Enter-key selection) it fires no
|
|
116
|
+
// `focus` event and the flag is simply lowered again — never left dangling
|
|
117
|
+
// for the next genuine focus.
|
|
118
|
+
let suppressFocusOpen = false;
|
|
119
|
+
|
|
120
|
+
function focusInputWithoutOpening() {
|
|
121
|
+
suppressFocusOpen = true;
|
|
122
|
+
inputEl?.focus();
|
|
123
|
+
suppressFocusOpen = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
function select(opt: ComboboxOption<T>) {
|
|
108
127
|
if (opt.disabled) return;
|
|
109
128
|
value = opt.value;
|
|
@@ -111,7 +130,7 @@
|
|
|
111
130
|
open = false;
|
|
112
131
|
activeIndex = -1;
|
|
113
132
|
onValueChange?.(opt.value);
|
|
114
|
-
|
|
133
|
+
focusInputWithoutOpening();
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
function clear() {
|
|
@@ -120,7 +139,7 @@
|
|
|
120
139
|
open = false;
|
|
121
140
|
activeIndex = -1;
|
|
122
141
|
onValueChange?.(null);
|
|
123
|
-
|
|
142
|
+
focusInputWithoutOpening();
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
function handleInput() {
|
|
@@ -133,6 +152,7 @@
|
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
function handleFocus() {
|
|
155
|
+
if (suppressFocusOpen) return;
|
|
136
156
|
if (!disabled) open = true;
|
|
137
157
|
}
|
|
138
158
|
|
|
@@ -126,6 +126,8 @@ declare const _default: {
|
|
|
126
126
|
readonly dismiss: "Hinweis ausblenden";
|
|
127
127
|
readonly startTour: "Geführte Tour starten";
|
|
128
128
|
readonly actionRequired: "Führe die markierte Aktion aus, um fortzufahren";
|
|
129
|
+
readonly filterPlaceholder: "Themen filtern…";
|
|
130
|
+
readonly noResults: "Keine passenden Themen";
|
|
129
131
|
};
|
|
130
132
|
readonly datepicker: {
|
|
131
133
|
readonly placeholder: "Datum wählen...";
|
package/dist/translations/de.js
CHANGED
|
@@ -125,7 +125,9 @@ export default {
|
|
|
125
125
|
infoAbout: 'Mehr Informationen zu {{label}}',
|
|
126
126
|
dismiss: 'Hinweis ausblenden',
|
|
127
127
|
startTour: 'Geführte Tour starten',
|
|
128
|
-
actionRequired: 'Führe die markierte Aktion aus, um fortzufahren'
|
|
128
|
+
actionRequired: 'Führe die markierte Aktion aus, um fortzufahren',
|
|
129
|
+
filterPlaceholder: 'Themen filtern…',
|
|
130
|
+
noResults: 'Keine passenden Themen'
|
|
129
131
|
},
|
|
130
132
|
datepicker: {
|
|
131
133
|
placeholder: 'Datum wählen...',
|
|
@@ -126,6 +126,8 @@ declare const _default: {
|
|
|
126
126
|
readonly dismiss: "Dismiss hint";
|
|
127
127
|
readonly startTour: "Start the guided tour";
|
|
128
128
|
readonly actionRequired: "Complete the highlighted action to continue";
|
|
129
|
+
readonly filterPlaceholder: "Filter topics…";
|
|
130
|
+
readonly noResults: "No matching topics";
|
|
129
131
|
};
|
|
130
132
|
readonly datepicker: {
|
|
131
133
|
readonly placeholder: "Select a date...";
|
package/dist/translations/en.js
CHANGED
|
@@ -125,7 +125,9 @@ export default {
|
|
|
125
125
|
infoAbout: 'More information about {{label}}',
|
|
126
126
|
dismiss: 'Dismiss hint',
|
|
127
127
|
startTour: 'Start the guided tour',
|
|
128
|
-
actionRequired: 'Complete the highlighted action to continue'
|
|
128
|
+
actionRequired: 'Complete the highlighted action to continue',
|
|
129
|
+
filterPlaceholder: 'Filter topics…',
|
|
130
|
+
noResults: 'No matching topics'
|
|
129
131
|
},
|
|
130
132
|
datepicker: {
|
|
131
133
|
placeholder: 'Select a date...',
|
|
@@ -109,7 +109,7 @@ export interface GuideControllerOptions {
|
|
|
109
109
|
storage?: GuideStorageAdapter;
|
|
110
110
|
/** Overlay stack to integrate with. @default the shared `overlayStack` singleton */
|
|
111
111
|
overlayStack?: GuideOverlayStackLike;
|
|
112
|
-
/** Force DEV-mode warnings on/off. @default `import.meta.env
|
|
112
|
+
/** Force DEV-mode warnings on/off. @default `import.meta.env?.DEV ?? false` */
|
|
113
113
|
dev?: boolean;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
@@ -112,7 +112,10 @@ export class GuideController {
|
|
|
112
112
|
constructor(options = {}) {
|
|
113
113
|
this.#storage = options.storage ?? createLocalStorageAdapter();
|
|
114
114
|
this.#overlayStack = options.overlayStack ?? overlayStack;
|
|
115
|
-
|
|
115
|
+
// `import.meta.env?.DEV` is `boolean | undefined` (undefined outside Vite);
|
|
116
|
+
// `?? false` keeps `#dev` a strict boolean and means non-Vite consumers
|
|
117
|
+
// simply get no dev warnings (rather than a crash).
|
|
118
|
+
this.#dev = options.dev ?? import.meta.env?.DEV ?? false;
|
|
116
119
|
this.#seen = new SvelteSet(this.#storage.load());
|
|
117
120
|
}
|
|
118
121
|
// ─── Target registry ──────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"@sveltejs/package": "^2.5.8",
|
|
92
92
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
93
93
|
"@tailwindcss/vite": "^4.3.1",
|
|
94
|
-
"@urbicon-ui/i18n": "6.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.
|
|
94
|
+
"@urbicon-ui/i18n": "6.5.0",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.5.0",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|