@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.
- package/AGENTS.md +6 -6
- package/dist/README.md +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Input/FieldOptions.svelte +92 -2
- package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
- package/dist/components/Input/README.md +66 -31
- package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
- package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
- package/dist/components/Input/index.css +58 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Pill/Pill.svelte +8 -2
- package/dist/components/Pill/Pill.svelte.d.ts +2 -0
- package/dist/components/Pill/README.md +35 -22
- package/dist/components/Pill/index.css +57 -18
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +132 -0
- package/docs/upgrading.md +6 -0
- package/package.json +11 -1
package/AGENTS.md
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
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/ #
|
|
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
|
|
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) —
|
|
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) —
|
|
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
|
|
@@ -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
|
+
…
|
|
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
|
+
};
|