@urbicon-ui/blocks 6.4.1 → 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/GuideArticle.svelte +3 -2
- package/dist/components/Guide/GuidePanel.svelte +98 -21
- 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/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/package.json +3 -3
|
@@ -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();
|
|
@@ -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,
|
|
@@ -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
|
);
|
|
@@ -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...";
|
|
@@ -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...',
|
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",
|