@marianmeres/stuic 3.167.0 → 3.169.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.
Files changed (115) hide show
  1. package/AGENTS.md +6 -6
  2. package/dist/README.md +1 -1
  3. package/dist/attachments/index.d.ts +1 -0
  4. package/dist/attachments/index.js +1 -0
  5. package/dist/attachments/long-press.d.ts +43 -0
  6. package/dist/attachments/long-press.js +107 -0
  7. package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
  8. package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
  9. package/dist/components/Breadcrumbs/README.md +174 -0
  10. package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
  11. package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
  12. package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
  13. package/dist/components/Breadcrumbs/i18n.js +40 -0
  14. package/dist/components/Breadcrumbs/index.css +84 -0
  15. package/dist/components/Breadcrumbs/index.d.ts +4 -0
  16. package/dist/components/Breadcrumbs/index.js +4 -0
  17. package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
  18. package/dist/components/Breadcrumbs/json-ld.js +56 -0
  19. package/dist/components/Collapsible/Collapsible.svelte +84 -15
  20. package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
  21. package/dist/components/Collapsible/README.md +82 -23
  22. package/dist/components/Collapsible/index.css +38 -0
  23. package/dist/components/Collapsible/index.d.ts +1 -1
  24. package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
  25. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
  26. package/dist/components/ContextMenu/README.md +159 -0
  27. package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
  28. package/dist/components/ContextMenu/i18n-sk.js +18 -0
  29. package/dist/components/ContextMenu/i18n.d.ts +30 -0
  30. package/dist/components/ContextMenu/i18n.js +38 -0
  31. package/dist/components/ContextMenu/index.css +26 -0
  32. package/dist/components/ContextMenu/index.d.ts +3 -0
  33. package/dist/components/ContextMenu/index.js +3 -0
  34. package/dist/components/CopyButton/CopyButton.svelte +278 -0
  35. package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
  36. package/dist/components/CopyButton/README.md +241 -0
  37. package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
  38. package/dist/components/CopyButton/i18n-sk.js +20 -0
  39. package/dist/components/CopyButton/i18n.d.ts +35 -0
  40. package/dist/components/CopyButton/i18n.js +43 -0
  41. package/dist/components/CopyButton/index.css +67 -0
  42. package/dist/components/CopyButton/index.d.ts +3 -0
  43. package/dist/components/CopyButton/index.js +3 -0
  44. package/dist/components/DataTable/DataTable.svelte +41 -34
  45. package/dist/components/DataTable/README.md +45 -0
  46. package/dist/components/DataTable/i18n-sk.js +1 -0
  47. package/dist/components/DataTable/i18n.d.ts +1 -0
  48. package/dist/components/DataTable/i18n.js +3 -0
  49. package/dist/components/DataTable/index.css +41 -3
  50. package/dist/components/EmptyState/EmptyState.svelte +121 -0
  51. package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
  52. package/dist/components/EmptyState/README.md +188 -0
  53. package/dist/components/EmptyState/index.css +154 -0
  54. package/dist/components/EmptyState/index.d.ts +1 -0
  55. package/dist/components/EmptyState/index.js +1 -0
  56. package/dist/components/Input/FieldOptions.svelte +92 -2
  57. package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
  58. package/dist/components/Input/README.md +66 -31
  59. package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
  60. package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
  61. package/dist/components/Input/index.css +58 -0
  62. package/dist/components/Pagination/Pagination.svelte +250 -0
  63. package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
  64. package/dist/components/Pagination/README.md +152 -0
  65. package/dist/components/Pagination/i18n-sk.d.ts +17 -0
  66. package/dist/components/Pagination/i18n-sk.js +24 -0
  67. package/dist/components/Pagination/i18n.d.ts +41 -0
  68. package/dist/components/Pagination/i18n.js +49 -0
  69. package/dist/components/Pagination/index.css +50 -0
  70. package/dist/components/Pagination/index.d.ts +4 -0
  71. package/dist/components/Pagination/index.js +4 -0
  72. package/dist/components/Pagination/pagination-range.d.ts +21 -0
  73. package/dist/components/Pagination/pagination-range.js +41 -0
  74. package/dist/components/Pill/Pill.svelte +8 -2
  75. package/dist/components/Pill/Pill.svelte.d.ts +2 -0
  76. package/dist/components/Pill/README.md +35 -22
  77. package/dist/components/Pill/index.css +57 -18
  78. package/dist/components/Stat/README.md +157 -0
  79. package/dist/components/Stat/Stat.svelte +226 -0
  80. package/dist/components/Stat/Stat.svelte.d.ts +54 -0
  81. package/dist/components/Stat/index.css +217 -0
  82. package/dist/components/Stat/index.d.ts +1 -0
  83. package/dist/components/Stat/index.js +1 -0
  84. package/dist/components/Stepper/README.md +181 -0
  85. package/dist/components/Stepper/Stepper.svelte +240 -0
  86. package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
  87. package/dist/components/Stepper/i18n-sk.d.ts +17 -0
  88. package/dist/components/Stepper/i18n-sk.js +21 -0
  89. package/dist/components/Stepper/i18n.d.ts +35 -0
  90. package/dist/components/Stepper/i18n.js +43 -0
  91. package/dist/components/Stepper/index.css +302 -0
  92. package/dist/components/Stepper/index.d.ts +3 -0
  93. package/dist/components/Stepper/index.js +3 -0
  94. package/dist/components/TrendChart/README.md +123 -0
  95. package/dist/components/TrendChart/TrendChart.svelte +114 -0
  96. package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
  97. package/dist/components/TrendChart/index.css +51 -0
  98. package/dist/components/TrendChart/index.d.ts +1 -0
  99. package/dist/components/TrendChart/index.js +1 -0
  100. package/dist/icons/index.d.ts +4 -0
  101. package/dist/icons/index.js +4 -0
  102. package/dist/index.css +7 -0
  103. package/dist/index.d.ts +7 -0
  104. package/dist/index.js +12 -0
  105. package/dist/utils/copy-to-clipboard.d.ts +43 -0
  106. package/dist/utils/copy-to-clipboard.js +102 -0
  107. package/dist/utils/index.d.ts +1 -0
  108. package/dist/utils/index.js +1 -0
  109. package/docs/architecture.md +1 -1
  110. package/docs/domains/attachments.md +41 -4
  111. package/docs/domains/components.md +280 -13
  112. package/docs/domains/utils.md +8 -6
  113. package/docs/maybe-todo.md +132 -0
  114. package/docs/upgrading.md +6 -0
  115. package/package.json +11 -1
package/AGENTS.md CHANGED
@@ -23,10 +23,10 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 63 component directories
26
+ ├── components/ # 73 component directories
27
27
  ├── actions/ # 15 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
- ├── utils/ # 44 utility modules
29
+ ├── utils/ # 45 utility modules
30
30
  ├── icons/ # Icon re-exports from @marianmeres/icons-fns
31
31
  ├── css/ # CSS-only presets (ratio-locked frame / letterbox)
32
32
  ├── index.css # Centralized CSS imports
@@ -55,8 +55,8 @@ Theme CSS files are not bundled in this package — they're provided by `@marian
55
55
  6. Use `dark:` Tailwind prefix when CSS vars handle dark mode
56
56
  7. Import CSS inside components — centralize in `src/lib/index.css`
57
57
  (**exception:** subpath-export components with optional peer deps — `MarkdownEditor`,
58
- `CommentInput` — import their own `index.css` locally so their styles don't ship to
59
- barrel-only consumers. Enforced by `src/lib/barrel-optional-peers.test.ts`.)
58
+ `CommentInput`, `TrendChart` — import their own `index.css` locally so their styles
59
+ don't ship to barrel-only consumers. Enforced by `src/lib/barrel-optional-peers.test.ts`.)
60
60
  8. Declare component tokens at `:root` that reference shared structural tokens (use fallback pattern instead)
61
61
 
62
62
  ### CSS Variable Pattern
@@ -129,12 +129,12 @@ Global tokens that control cross-component visual properties. Defined in `src/li
129
129
 
130
130
  ### Domain Docs
131
131
 
132
- - [Components](./docs/domains/components.md) — 63 component directories, Props pattern, snippets
132
+ - [Components](./docs/domains/components.md) — 73 component directories, Props pattern, snippets
133
133
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
134
134
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
135
135
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
136
136
  - [Attachments](./docs/domains/attachments.md) — `{@attach}` DOM helpers (preferred for new ones)
137
- - [Utils](./docs/domains/utils.md) — 44 utility modules
137
+ - [Utils](./docs/domains/utils.md) — 45 utility modules
138
138
 
139
139
  ### Reference
140
140
 
package/dist/README.md CHANGED
@@ -43,7 +43,7 @@ npm install @marianmeres/stuic
43
43
  - **FieldRadios** - Radio button group
44
44
  - **FieldFile** - File upload input
45
45
  - **FieldAssets** - Multi-file upload with preview
46
- - **FieldOptions** - Modal-based multi-select picker
46
+ - **FieldOptions** - Modal-based multi-select picker (optional inline `chips` display)
47
47
  - **FieldKeyValues** - Key-value pairs editor with JSON serialization
48
48
  - **FieldSwitch** - Toggle switch within a form
49
49
  - **Fieldset** - Group of form fields with legend
@@ -1 +1,2 @@
1
1
  export * from "./auto-height.js";
2
+ export * from "./long-press.js";
@@ -1 +1,2 @@
1
1
  export * from "./auto-height.js";
2
+ export * from "./long-press.js";
@@ -0,0 +1,43 @@
1
+ import type { Attachment } from "svelte/attachments";
2
+ export interface LongPressOptions {
3
+ /** Called when the press exceeds `duration` without moving beyond `moveTolerance`. */
4
+ onLongPress: (event: PointerEvent) => void;
5
+ /** How long (ms) the pointer must stay down before firing. Default 500. */
6
+ duration?: number;
7
+ /** Movement (px, straight-line) that cancels the pending press. Default 10. */
8
+ moveTolerance?: number;
9
+ /** Pointer types that can long-press. Default `["touch", "pen"]` (mouse has right-click). */
10
+ pointerTypes?: string[];
11
+ /**
12
+ * Swallow the `click` that some platforms fire when the finger lifts after a
13
+ * long-press, so it can't activate whatever sits under it (link, button) right
14
+ * after the long-press action ran. Default true.
15
+ */
16
+ suppressClick?: boolean;
17
+ }
18
+ /**
19
+ * Svelte attachment (factory) that detects a long-press on the host element and calls
20
+ * `onLongPress` with the initiating `pointerdown` event (its `clientX`/`clientY` is
21
+ * where the press happened).
22
+ *
23
+ * Semantics: only primary pointers of the configured `pointerTypes` arm the timer;
24
+ * lifting, cancellation (e.g. the platform turning the gesture into a scroll), or
25
+ * moving beyond `moveTolerance` disarms it. A native `contextmenu` event on the host
26
+ * (Android fires it on long-press by itself) also disarms, so a host that handles
27
+ * `contextmenu` never gets both paths for one gesture.
28
+ *
29
+ * The browser's own long-press behaviors are NOT prevented here — that is CSS the
30
+ * consumer owns: `-webkit-touch-callout: none` (iOS link/image callout) and
31
+ * `user-select: none` (text-selection long-press) on the host.
32
+ *
33
+ * @example
34
+ * ```svelte
35
+ * <div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>...</div>
36
+ * ```
37
+ *
38
+ * Conditional usage (a falsy value means "no attachment"):
39
+ * ```svelte
40
+ * <div {@attach enabled && longPress({ onLongPress })}>...</div>
41
+ * ```
42
+ */
43
+ export declare function longPress(options: LongPressOptions): Attachment<HTMLElement>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Svelte attachment (factory) that detects a long-press on the host element and calls
3
+ * `onLongPress` with the initiating `pointerdown` event (its `clientX`/`clientY` is
4
+ * where the press happened).
5
+ *
6
+ * Semantics: only primary pointers of the configured `pointerTypes` arm the timer;
7
+ * lifting, cancellation (e.g. the platform turning the gesture into a scroll), or
8
+ * moving beyond `moveTolerance` disarms it. A native `contextmenu` event on the host
9
+ * (Android fires it on long-press by itself) also disarms, so a host that handles
10
+ * `contextmenu` never gets both paths for one gesture.
11
+ *
12
+ * The browser's own long-press behaviors are NOT prevented here — that is CSS the
13
+ * consumer owns: `-webkit-touch-callout: none` (iOS link/image callout) and
14
+ * `user-select: none` (text-selection long-press) on the host.
15
+ *
16
+ * @example
17
+ * ```svelte
18
+ * <div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>...</div>
19
+ * ```
20
+ *
21
+ * Conditional usage (a falsy value means "no attachment"):
22
+ * ```svelte
23
+ * <div {@attach enabled && longPress({ onLongPress })}>...</div>
24
+ * ```
25
+ */
26
+ export function longPress(options) {
27
+ return (node) => {
28
+ const { onLongPress, duration = 500, moveTolerance = 10, pointerTypes = ["touch", "pen"], suppressClick = true, } = options;
29
+ let timer;
30
+ let startEvent;
31
+ let fired = false;
32
+ let suppressing = false;
33
+ const disarm = () => {
34
+ if (timer !== undefined)
35
+ clearTimeout(timer);
36
+ timer = undefined;
37
+ startEvent = undefined;
38
+ window.removeEventListener("pointermove", onMove);
39
+ window.removeEventListener("pointerup", onUp);
40
+ window.removeEventListener("pointercancel", onPointerCancel);
41
+ };
42
+ const onMove = (e) => {
43
+ if (!startEvent || e.pointerId !== startEvent.pointerId || fired)
44
+ return;
45
+ const dist = Math.hypot(e.clientX - startEvent.clientX, e.clientY - startEvent.clientY);
46
+ if (dist > moveTolerance)
47
+ disarm();
48
+ };
49
+ // The follow-up click (if the platform fires one) lands right after the
50
+ // gesture ends — close the suppress window soon after, so a stale flag can
51
+ // never swallow a later genuine click.
52
+ const scheduleSuppressEnd = () => {
53
+ if (fired && suppressing)
54
+ setTimeout(() => (suppressing = false), 150);
55
+ };
56
+ const onUp = (e) => {
57
+ if (!startEvent || e.pointerId !== startEvent.pointerId)
58
+ return;
59
+ scheduleSuppressEnd();
60
+ disarm();
61
+ };
62
+ const onPointerCancel = (e) => {
63
+ if (!startEvent || e.pointerId !== startEvent.pointerId)
64
+ return;
65
+ scheduleSuppressEnd();
66
+ disarm();
67
+ };
68
+ const onDown = (e) => {
69
+ if (!e.isPrimary || !pointerTypes.includes(e.pointerType))
70
+ return;
71
+ disarm();
72
+ fired = false;
73
+ startEvent = e;
74
+ window.addEventListener("pointermove", onMove);
75
+ window.addEventListener("pointerup", onUp);
76
+ window.addEventListener("pointercancel", onPointerCancel);
77
+ timer = setTimeout(() => {
78
+ timer = undefined;
79
+ fired = true;
80
+ suppressing = suppressClick;
81
+ onLongPress(e);
82
+ }, duration);
83
+ };
84
+ // Android synthesizes contextmenu on long-press by itself — let that path win.
85
+ const onNativeContextMenu = () => {
86
+ scheduleSuppressEnd();
87
+ disarm();
88
+ };
89
+ const onClickCapture = (e) => {
90
+ if (!suppressing)
91
+ return;
92
+ suppressing = false;
93
+ e.preventDefault();
94
+ e.stopPropagation();
95
+ };
96
+ node.addEventListener("pointerdown", onDown);
97
+ node.addEventListener("contextmenu", onNativeContextMenu);
98
+ window.addEventListener("click", onClickCapture, { capture: true });
99
+ return () => {
100
+ disarm();
101
+ suppressing = false;
102
+ node.removeEventListener("pointerdown", onDown);
103
+ node.removeEventListener("contextmenu", onNativeContextMenu);
104
+ window.removeEventListener("click", onClickCapture, { capture: true });
105
+ };
106
+ };
107
+ }
@@ -0,0 +1,198 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { Snippet } from "svelte";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type { BreadcrumbsJsonLdOptions } from "./json-ld.js";
6
+
7
+ /** One crumb of the trail. */
8
+ export interface BreadcrumbItem {
9
+ /** Visible label (also the `name` in the JSON-LD output). */
10
+ label: string;
11
+ /**
12
+ * Link target. Omit to render a plain (non-link) crumb — typical for the
13
+ * last item, the current page.
14
+ */
15
+ href?: string;
16
+ }
17
+
18
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
19
+ /** The trail, first-to-current. The last item is marked as the current page. */
20
+ items: BreadcrumbItem[];
21
+ /** Separator text between crumbs (default "/"). See also `renderSeparator`. */
22
+ separator?: string;
23
+ /**
24
+ * Collapse the middle of a long trail into an expandable ellipsis button when
25
+ * there are more than `maxItems` crumbs. `0` (default) never collapses.
26
+ */
27
+ maxItems?: number;
28
+ /** How many leading crumbs stay visible when collapsed (default 1). */
29
+ itemsBeforeCollapse?: number;
30
+ /** How many trailing crumbs stay visible when collapsed (default 1). */
31
+ itemsAfterCollapse?: number;
32
+ /**
33
+ * Render schema.org `BreadcrumbList` JSON-LD (a
34
+ * `<script type="application/ld+json">`) along with the trail — the
35
+ * structured data search engines read. Pass `{ baseUrl }` to resolve
36
+ * relative `href`s to the absolute URLs Google recommends. Alternatively
37
+ * use the exported `breadcrumbsJsonLdScript()` helper in `<svelte:head>`.
38
+ */
39
+ jsonLd?: boolean | BreadcrumbsJsonLdOptions;
40
+ /** i18n translate function (see `createBreadcrumbsT`). */
41
+ t?: TranslateFn;
42
+ /** Override the rendering of every crumb; receives `(item, index, isLast)`. */
43
+ renderItem?: Snippet<[BreadcrumbItem, number, boolean]>;
44
+ /** Override the separator between crumbs. */
45
+ renderSeparator?: Snippet;
46
+ /** Skip all default styling */
47
+ unstyled?: boolean;
48
+ /** Additional CSS classes */
49
+ class?: string;
50
+ /** Class for every `<li>` */
51
+ classItem?: string;
52
+ /** Class for every crumb link */
53
+ classLink?: string;
54
+ /** Extra class for the current (last) crumb */
55
+ classCurrent?: string;
56
+ /** Class for the separator */
57
+ classSeparator?: string;
58
+ /** Bindable element reference */
59
+ el?: HTMLElement;
60
+ }
61
+
62
+ type Entry = { item: BreadcrumbItem; index: number } | "ellipsis";
63
+ </script>
64
+
65
+ <script lang="ts">
66
+ import { twMerge } from "../../utils/tw-merge.js";
67
+ import { t_default } from "./i18n.js";
68
+ import { breadcrumbsJsonLdScript } from "./json-ld.js";
69
+
70
+ let {
71
+ items,
72
+ separator = "/",
73
+ maxItems = 0,
74
+ itemsBeforeCollapse = 1,
75
+ itemsAfterCollapse = 1,
76
+ jsonLd = false,
77
+ t = t_default,
78
+ renderItem,
79
+ renderSeparator,
80
+ unstyled = false,
81
+ class: classProp,
82
+ classItem: classItemProp,
83
+ classLink: classLinkProp,
84
+ classCurrent: classCurrentProp,
85
+ classSeparator: classSeparatorProp,
86
+ el = $bindable(),
87
+ ...rest
88
+ }: Props = $props();
89
+
90
+ // expansion is keyed on the items reference, so a new trail (navigation)
91
+ // automatically collapses again ($state.raw: plain $state would proxy the
92
+ // stored array and break the identity comparison)
93
+ let _expandedFor: BreadcrumbItem[] | undefined = $state.raw();
94
+ let _expanded = $derived(_expandedFor === items);
95
+
96
+ let _collapsible = $derived(
97
+ maxItems > 0 &&
98
+ items.length > maxItems &&
99
+ items.length > itemsBeforeCollapse + itemsAfterCollapse
100
+ );
101
+
102
+ let _entries: Entry[] = $derived.by(() => {
103
+ const all: Entry[] = items.map((item, index) => ({ item, index }));
104
+ if (!_collapsible || _expanded) return all;
105
+ return [
106
+ ...all.slice(0, itemsBeforeCollapse),
107
+ "ellipsis",
108
+ ...all.slice(items.length - itemsAfterCollapse),
109
+ ];
110
+ });
111
+
112
+ let _class = $derived(unstyled ? classProp : twMerge("stuic-breadcrumbs", classProp));
113
+ let _classItem = $derived(
114
+ unstyled ? classItemProp : twMerge("stuic-breadcrumbs-item", classItemProp)
115
+ );
116
+ let _classLink = $derived(
117
+ unstyled ? classLinkProp : twMerge("stuic-breadcrumbs-link", classLinkProp)
118
+ );
119
+ let _classSeparator = $derived(
120
+ unstyled
121
+ ? classSeparatorProp
122
+ : twMerge("stuic-breadcrumbs-separator", classSeparatorProp)
123
+ );
124
+
125
+ function _classCrumb(isLast: boolean, isLink: boolean) {
126
+ const current = isLast
127
+ ? unstyled
128
+ ? classCurrentProp
129
+ : twMerge("stuic-breadcrumbs-current", classCurrentProp)
130
+ : undefined;
131
+ const link = isLink ? _classLink : undefined;
132
+ return twMerge(link, current) || undefined;
133
+ }
134
+
135
+ let _jsonLdScript = $derived(
136
+ jsonLd ? breadcrumbsJsonLdScript(items, jsonLd === true ? undefined : jsonLd) : ""
137
+ );
138
+ </script>
139
+
140
+ {#if items.length}
141
+ <nav
142
+ bind:this={el}
143
+ class={_class}
144
+ aria-label={t("breadcrumbs", null, "Breadcrumb")}
145
+ {...rest}
146
+ >
147
+ <ol class={!unstyled ? "stuic-breadcrumbs-list" : undefined}>
148
+ {#each _entries as entry, i (entry === "ellipsis" ? "ellipsis" : entry.index)}
149
+ <li
150
+ class={_classItem}
151
+ data-current={entry !== "ellipsis" && entry.index === items.length - 1
152
+ ? "true"
153
+ : undefined}
154
+ >
155
+ {#if i > 0}
156
+ <span class={_classSeparator} aria-hidden="true">
157
+ {#if renderSeparator}{@render renderSeparator()}{:else}{separator}{/if}
158
+ </span>
159
+ {/if}
160
+ {#if entry === "ellipsis"}
161
+ <button
162
+ type="button"
163
+ class={!unstyled ? "stuic-breadcrumbs-ellipsis" : undefined}
164
+ aria-label={t("show_all", null, "Show all breadcrumbs")}
165
+ onclick={() => (_expandedFor = items)}
166
+ >
167
+ &hellip;
168
+ </button>
169
+ {:else}
170
+ {@const isLast = entry.index === items.length - 1}
171
+ {#if renderItem}
172
+ {@render renderItem(entry.item, entry.index, isLast)}
173
+ {:else if entry.item.href}
174
+ <a
175
+ href={entry.item.href}
176
+ class={_classCrumb(isLast, true)}
177
+ aria-current={isLast ? "page" : undefined}
178
+ >
179
+ {entry.item.label}
180
+ </a>
181
+ {:else}
182
+ <span
183
+ class={_classCrumb(isLast, false)}
184
+ aria-current={isLast ? "page" : undefined}
185
+ >
186
+ {entry.item.label}
187
+ </span>
188
+ {/if}
189
+ {/if}
190
+ </li>
191
+ {/each}
192
+ </ol>
193
+ {#if _jsonLdScript}
194
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- helper escapes angle brackets -->
195
+ {@html _jsonLdScript}
196
+ {/if}
197
+ </nav>
198
+ {/if}
@@ -0,0 +1,60 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import type { Snippet } from "svelte";
3
+ import type { TranslateFn } from "../../types.js";
4
+ import type { BreadcrumbsJsonLdOptions } from "./json-ld.js";
5
+ /** One crumb of the trail. */
6
+ export interface BreadcrumbItem {
7
+ /** Visible label (also the `name` in the JSON-LD output). */
8
+ label: string;
9
+ /**
10
+ * Link target. Omit to render a plain (non-link) crumb — typical for the
11
+ * last item, the current page.
12
+ */
13
+ href?: string;
14
+ }
15
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
16
+ /** The trail, first-to-current. The last item is marked as the current page. */
17
+ items: BreadcrumbItem[];
18
+ /** Separator text between crumbs (default "/"). See also `renderSeparator`. */
19
+ separator?: string;
20
+ /**
21
+ * Collapse the middle of a long trail into an expandable ellipsis button when
22
+ * there are more than `maxItems` crumbs. `0` (default) never collapses.
23
+ */
24
+ maxItems?: number;
25
+ /** How many leading crumbs stay visible when collapsed (default 1). */
26
+ itemsBeforeCollapse?: number;
27
+ /** How many trailing crumbs stay visible when collapsed (default 1). */
28
+ itemsAfterCollapse?: number;
29
+ /**
30
+ * Render schema.org `BreadcrumbList` JSON-LD (a
31
+ * `<script type="application/ld+json">`) along with the trail — the
32
+ * structured data search engines read. Pass `{ baseUrl }` to resolve
33
+ * relative `href`s to the absolute URLs Google recommends. Alternatively
34
+ * use the exported `breadcrumbsJsonLdScript()` helper in `<svelte:head>`.
35
+ */
36
+ jsonLd?: boolean | BreadcrumbsJsonLdOptions;
37
+ /** i18n translate function (see `createBreadcrumbsT`). */
38
+ t?: TranslateFn;
39
+ /** Override the rendering of every crumb; receives `(item, index, isLast)`. */
40
+ renderItem?: Snippet<[BreadcrumbItem, number, boolean]>;
41
+ /** Override the separator between crumbs. */
42
+ renderSeparator?: Snippet;
43
+ /** Skip all default styling */
44
+ unstyled?: boolean;
45
+ /** Additional CSS classes */
46
+ class?: string;
47
+ /** Class for every `<li>` */
48
+ classItem?: string;
49
+ /** Class for every crumb link */
50
+ classLink?: string;
51
+ /** Extra class for the current (last) crumb */
52
+ classCurrent?: string;
53
+ /** Class for the separator */
54
+ classSeparator?: string;
55
+ /** Bindable element reference */
56
+ el?: HTMLElement;
57
+ }
58
+ declare const Breadcrumbs: import("svelte").Component<Props, {}, "el">;
59
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
60
+ export default Breadcrumbs;
@@ -0,0 +1,174 @@
1
+ # Breadcrumbs
2
+
3
+ Breadcrumb navigation trail — `<nav>` + ordered list of links with the current page
4
+ marked, an optional collapse of long trails into an expandable ellipsis, and
5
+ schema.org `BreadcrumbList` JSON-LD helpers for SEO.
6
+
7
+ ## Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ | --------------------- | ------------------------ | ------- | ------------------------------------------------------------------ |
11
+ | `items` | `BreadcrumbItem[]` | - | The trail, first-to-current; the last item is the current page |
12
+ | `separator` | `string` | `"/"` | Separator text between crumbs |
13
+ | `maxItems` | `number` | `0` | Collapse when there are more crumbs than this; `0` never collapses |
14
+ | `itemsBeforeCollapse` | `number` | `1` | Leading crumbs kept visible when collapsed |
15
+ | `itemsAfterCollapse` | `number` | `1` | Trailing crumbs kept visible when collapsed |
16
+ | `jsonLd` | `boolean \| { baseUrl }` | `false` | Render schema.org `BreadcrumbList` JSON-LD along with the trail |
17
+ | `t` | `TranslateFn` | English | i18n translate function (see below) |
18
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
19
+ | `class` | `string` | - | Additional CSS classes (merged via twMerge) |
20
+ | `classItem` | `string` | - | Class for every `<li>` |
21
+ | `classLink` | `string` | - | Class for every crumb link |
22
+ | `classCurrent` | `string` | - | Extra class for the current (last) crumb |
23
+ | `classSeparator` | `string` | - | Class for the separator |
24
+ | `el` | `HTMLElement` | - | Element reference (bindable) |
25
+
26
+ `BreadcrumbItem`: `{ label: string; href?: string }` — omit `href` to render a plain
27
+ (non-link) crumb, typical for the last item (the current page).
28
+
29
+ ## Snippet Props
30
+
31
+ | Snippet | Description |
32
+ | ----------------- | ------------------------------------------------------ |
33
+ | `renderItem` | Override every crumb; receives `(item, index, isLast)` |
34
+ | `renderSeparator` | Override the separator between crumbs |
35
+
36
+ ## Usage
37
+
38
+ ### Basic
39
+
40
+ ```svelte
41
+ <script lang="ts">
42
+ import { Breadcrumbs } from "@marianmeres/stuic";
43
+
44
+ const items = [
45
+ { label: "Home", href: "/" },
46
+ { label: "Products", href: "/products" },
47
+ { label: "Phones" }, // current page — no href
48
+ ];
49
+ </script>
50
+
51
+ <Breadcrumbs {items} />
52
+ ```
53
+
54
+ ### Custom separator
55
+
56
+ ```svelte
57
+ <Breadcrumbs {items} separator="›" />
58
+
59
+ <!-- or any markup -->
60
+ <Breadcrumbs {items}>
61
+ {#snippet renderSeparator()}
62
+ <ChevronIcon />
63
+ {/snippet}
64
+ </Breadcrumbs>
65
+ ```
66
+
67
+ ### Long trails (collapse)
68
+
69
+ Trails longer than `maxItems` collapse their middle into an ellipsis button;
70
+ clicking it expands the full trail. A new `items` array (navigation) collapses it
71
+ again.
72
+
73
+ ```svelte
74
+ <Breadcrumbs {items} maxItems={4} itemsBeforeCollapse={1} itemsAfterCollapse={2} />
75
+ ```
76
+
77
+ ### SEO structured data (JSON-LD)
78
+
79
+ Search engines read breadcrumb trails from schema.org `BreadcrumbList` structured
80
+ data. Either let the component render it inline (JSON-LD is valid anywhere in the
81
+ document):
82
+
83
+ ```svelte
84
+ <Breadcrumbs {items} jsonLd={{ baseUrl: page.url.origin }} />
85
+ ```
86
+
87
+ ...or keep it in the head via the pure helper (SvelteKit):
88
+
89
+ ```svelte
90
+ <script lang="ts">
91
+ import { Breadcrumbs, breadcrumbsJsonLdScript } from "@marianmeres/stuic";
92
+ import { page } from "$app/state";
93
+ </script>
94
+
95
+ <svelte:head>
96
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- helper escapes angle brackets -->
97
+ {@html breadcrumbsJsonLdScript(items, { baseUrl: page.url.origin })}
98
+ </svelte:head>
99
+
100
+ <Breadcrumbs {items} />
101
+ ```
102
+
103
+ Notes:
104
+
105
+ - crumbs without `href` (the current page) are emitted without `item`, exactly per
106
+ Google's guidelines;
107
+ - `baseUrl` resolves relative `href`s to the absolute URLs Google recommends;
108
+ - the emitted JSON is `\uXXXX`-escaped (`<`, `>`, `&`), so it is safe for `{@html}`
109
+ — a label can never break out of the script element;
110
+ - `breadcrumbsJsonLd(items, options?)` returns the raw object if you prefer to
111
+ serialize yourself.
112
+
113
+ ### Custom crumb rendering
114
+
115
+ ```svelte
116
+ <Breadcrumbs {items}>
117
+ {#snippet renderItem(item, index, isLast)}
118
+ {#if index === 0}
119
+ <a href={item.href} aria-label={item.label}><HomeIcon /></a>
120
+ {:else if isLast}
121
+ <span aria-current="page">{item.label}</span>
122
+ {:else}
123
+ <a href={item.href}>{item.label}</a>
124
+ {/if}
125
+ {/snippet}
126
+ </Breadcrumbs>
127
+ ```
128
+
129
+ ### i18n
130
+
131
+ Built-in English; bundled Slovak (`BREADCRUMBS_MESSAGES_SK`) is opt-in.
132
+
133
+ ```svelte
134
+ <script lang="ts">
135
+ import {
136
+ Breadcrumbs,
137
+ createBreadcrumbsT,
138
+ BREADCRUMBS_MESSAGES_SK,
139
+ } from "@marianmeres/stuic";
140
+ const t = createBreadcrumbsT(BREADCRUMBS_MESSAGES_SK);
141
+ </script>
142
+
143
+ <Breadcrumbs {items} {t} />
144
+ ```
145
+
146
+ ## Accessibility
147
+
148
+ - Renders a `<nav aria-label="Breadcrumb">` landmark (label localized via `t`)
149
+ containing an ordered list — the WAI-ARIA APG breadcrumb pattern.
150
+ - The last crumb carries `aria-current="page"` (link or plain span alike).
151
+ - Separators are `aria-hidden`.
152
+ - The collapse ellipsis is a real `<button>` with a localized `aria-label`.
153
+
154
+ ## Exports
155
+
156
+ Besides the component: `breadcrumbsJsonLd` / `breadcrumbsJsonLdScript` (schema.org
157
+ `BreadcrumbList` helpers), `createBreadcrumbsT` + message catalogs (i18n).
158
+
159
+ ## CSS Variables
160
+
161
+ | Variable | Default | Description |
162
+ | ----------------------------------------- | -------------------------------- | ------------------------------------ |
163
+ | `--stuic-breadcrumbs-gap` | `0.375rem` | Gap between crumbs and separators |
164
+ | `--stuic-breadcrumbs-font-size` | `--text-sm` | Trail font size |
165
+ | `--stuic-breadcrumbs-text` | `--stuic-color-muted-foreground` | Link / plain crumb color |
166
+ | `--stuic-breadcrumbs-text-hover` | `--stuic-color-foreground` | Link hover color |
167
+ | `--stuic-breadcrumbs-text-current` | `--stuic-color-foreground` | Current crumb color |
168
+ | `--stuic-breadcrumbs-font-weight-current` | `500` | Current crumb font weight |
169
+ | `--stuic-breadcrumbs-separator-text` | `--stuic-color-muted-foreground` | Separator color |
170
+ | `--stuic-breadcrumbs-transition` | `--stuic-transition` | Hover transition duration (fallback) |
171
+
172
+ ## Data Attributes
173
+
174
+ - `data-current` - `"true"` on the current (last) crumb's `<li>`
@@ -0,0 +1,17 @@
1
+ import type { BreadcrumbsMessages } from "./i18n.js";
2
+ /**
3
+ * Slovak message catalog for `Breadcrumbs`. Opt-in — English stays the built-in
4
+ * default, and this module is only pulled into a bundle when it is actually
5
+ * imported (the component itself never references it).
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import { Breadcrumbs, createBreadcrumbsT, BREADCRUMBS_MESSAGES_SK } from "@marianmeres/stuic";
11
+ * const t = createBreadcrumbsT(BREADCRUMBS_MESSAGES_SK);
12
+ * </script>
13
+ *
14
+ * <Breadcrumbs {items} {t} />
15
+ * ```
16
+ */
17
+ export declare const BREADCRUMBS_MESSAGES_SK: BreadcrumbsMessages;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Slovak message catalog for `Breadcrumbs`. Opt-in — English stays the built-in
3
+ * default, and this module is only pulled into a bundle when it is actually
4
+ * imported (the component itself never references it).
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * import { Breadcrumbs, createBreadcrumbsT, BREADCRUMBS_MESSAGES_SK } from "@marianmeres/stuic";
10
+ * const t = createBreadcrumbsT(BREADCRUMBS_MESSAGES_SK);
11
+ * </script>
12
+ *
13
+ * <Breadcrumbs {items} {t} />
14
+ * ```
15
+ */
16
+ export const BREADCRUMBS_MESSAGES_SK = {
17
+ breadcrumbs: "Omrvinková navigácia",
18
+ show_all: "Zobraziť celú cestu",
19
+ };