@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
@@ -0,0 +1,31 @@
1
+ import type { TranslateFn } from "../../types.js";
2
+ /**
3
+ * The built-in (English) message catalog of `Breadcrumbs`. Also the fallback of
4
+ * every other bundled locale, so a locale missing a key still renders text.
5
+ */
6
+ export declare const BREADCRUMBS_MESSAGES_EN: {
7
+ breadcrumbs: string;
8
+ show_all: string;
9
+ };
10
+ /** Every message key `Breadcrumbs` may look up. */
11
+ export type BreadcrumbsMessageKey = keyof typeof BREADCRUMBS_MESSAGES_EN;
12
+ /** A (possibly partial) catalog for one locale. */
13
+ export type BreadcrumbsMessages = Record<BreadcrumbsMessageKey, string>;
14
+ /**
15
+ * Builds the `t` prop of `Breadcrumbs` from a message catalog. Unknown or
16
+ * untranslated keys fall back to `fallbackMessages` (English by default), so a
17
+ * catalog may safely be partial and never renders a raw key.
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <script>
22
+ * import { Breadcrumbs, createBreadcrumbsT, BREADCRUMBS_MESSAGES_SK } from "@marianmeres/stuic";
23
+ * const t = createBreadcrumbsT(BREADCRUMBS_MESSAGES_SK);
24
+ * </script>
25
+ *
26
+ * <Breadcrumbs {items} {t} />
27
+ * ```
28
+ */
29
+ export declare function createBreadcrumbsT(messages: Partial<BreadcrumbsMessages> | Record<string, string>, fallbackMessages?: Partial<BreadcrumbsMessages> | Record<string, string>): TranslateFn;
30
+ /** The component's built-in English `t`. */
31
+ export declare const t_default: TranslateFn;
@@ -0,0 +1,40 @@
1
+ import { isPlainObject } from "../../utils/is-plain-object.js";
2
+ import { replaceMap } from "../../utils/replace-map.js";
3
+ /**
4
+ * The built-in (English) message catalog of `Breadcrumbs`. Also the fallback of
5
+ * every other bundled locale, so a locale missing a key still renders text.
6
+ */
7
+ export const BREADCRUMBS_MESSAGES_EN = {
8
+ // singular "Breadcrumb" is the WAI-ARIA APG recommended landmark label
9
+ breadcrumbs: "Breadcrumb",
10
+ show_all: "Show all breadcrumbs",
11
+ };
12
+ /**
13
+ * Builds the `t` prop of `Breadcrumbs` from a message catalog. Unknown or
14
+ * untranslated keys fall back to `fallbackMessages` (English by default), so a
15
+ * catalog may safely be partial and never renders a raw key.
16
+ *
17
+ * @example
18
+ * ```svelte
19
+ * <script>
20
+ * import { Breadcrumbs, createBreadcrumbsT, BREADCRUMBS_MESSAGES_SK } from "@marianmeres/stuic";
21
+ * const t = createBreadcrumbsT(BREADCRUMBS_MESSAGES_SK);
22
+ * </script>
23
+ *
24
+ * <Breadcrumbs {items} {t} />
25
+ * ```
26
+ */
27
+ export function createBreadcrumbsT(messages, fallbackMessages = BREADCRUMBS_MESSAGES_EN) {
28
+ return (k, values = null, fallback = "") => {
29
+ const out = messages[k] ??
30
+ fallbackMessages[k] ??
31
+ (typeof fallback === "string" ? fallback : k);
32
+ return isPlainObject(values)
33
+ ? replaceMap(out, values, {
34
+ preSearchKeyTransform: (k) => `{${k}}`,
35
+ })
36
+ : out;
37
+ };
38
+ }
39
+ /** The component's built-in English `t`. */
40
+ export const t_default = createBreadcrumbsT(BREADCRUMBS_MESSAGES_EN);
@@ -0,0 +1,84 @@
1
+ /* ============================================================================
2
+ BREADCRUMBS COMPONENT TOKENS
3
+ Override globally: :root { --stuic-breadcrumbs-gap: 0.5rem; }
4
+ Override locally: <Breadcrumbs style="--stuic-breadcrumbs-font-size: 1rem;">
5
+ ============================================================================ */
6
+
7
+ /* prettier-ignore */
8
+ :root {
9
+ --stuic-breadcrumbs-gap: 0.375rem;
10
+ --stuic-breadcrumbs-font-size: var(--text-sm);
11
+
12
+ /* links + plain (non-current) crumbs + inherited by the ellipsis button */
13
+ --stuic-breadcrumbs-text: var(--stuic-color-muted-foreground);
14
+ --stuic-breadcrumbs-text-hover: var(--stuic-color-foreground);
15
+
16
+ /* the current (last) crumb */
17
+ --stuic-breadcrumbs-text-current: var(--stuic-color-foreground);
18
+ --stuic-breadcrumbs-font-weight-current: 500;
19
+
20
+ --stuic-breadcrumbs-separator-text: var(--stuic-color-muted-foreground);
21
+ }
22
+
23
+ @layer components {
24
+ .stuic-breadcrumbs-list {
25
+ display: flex;
26
+ align-items: center;
27
+ flex-wrap: wrap;
28
+ gap: var(--stuic-breadcrumbs-gap);
29
+ margin: 0;
30
+ padding: 0;
31
+ list-style: none;
32
+ font-size: var(--stuic-breadcrumbs-font-size);
33
+ }
34
+
35
+ .stuic-breadcrumbs-item {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ gap: var(--stuic-breadcrumbs-gap);
39
+ color: var(--stuic-breadcrumbs-text);
40
+ }
41
+
42
+ .stuic-breadcrumbs-link {
43
+ color: inherit;
44
+ text-decoration: none;
45
+ transition: color var(--stuic-breadcrumbs-transition, var(--stuic-transition));
46
+ }
47
+
48
+ .stuic-breadcrumbs-link:hover {
49
+ color: var(--stuic-breadcrumbs-text-hover);
50
+ text-decoration: underline;
51
+ }
52
+
53
+ .stuic-breadcrumbs-current {
54
+ color: var(--stuic-breadcrumbs-text-current);
55
+ font-weight: var(--stuic-breadcrumbs-font-weight-current);
56
+ }
57
+
58
+ /* current crumb that is also a link keeps its current color on hover */
59
+ .stuic-breadcrumbs-link.stuic-breadcrumbs-current:hover {
60
+ color: var(--stuic-breadcrumbs-text-current);
61
+ }
62
+
63
+ .stuic-breadcrumbs-separator {
64
+ color: var(--stuic-breadcrumbs-separator-text);
65
+ user-select: none;
66
+ }
67
+
68
+ .stuic-breadcrumbs-ellipsis {
69
+ appearance: none;
70
+ background: none;
71
+ border: none;
72
+ padding: 0;
73
+ margin: 0;
74
+ font: inherit;
75
+ color: inherit;
76
+ cursor: pointer;
77
+ line-height: 1;
78
+ transition: color var(--stuic-breadcrumbs-transition, var(--stuic-transition));
79
+ }
80
+
81
+ .stuic-breadcrumbs-ellipsis:hover {
82
+ color: var(--stuic-breadcrumbs-text-hover);
83
+ }
84
+ }
@@ -0,0 +1,4 @@
1
+ export { default as Breadcrumbs, type Props as BreadcrumbsProps, type BreadcrumbItem, } from "./Breadcrumbs.svelte";
2
+ export { breadcrumbsJsonLd, breadcrumbsJsonLdScript, type BreadcrumbsJsonLdOptions, type BreadcrumbListItemJsonLd, type BreadcrumbListJsonLd, } from "./json-ld.js";
3
+ export { createBreadcrumbsT, BREADCRUMBS_MESSAGES_EN, type BreadcrumbsMessageKey, type BreadcrumbsMessages, } from "./i18n.js";
4
+ export { BREADCRUMBS_MESSAGES_SK } from "./i18n-sk.js";
@@ -0,0 +1,4 @@
1
+ export { default as Breadcrumbs, } from "./Breadcrumbs.svelte";
2
+ export { breadcrumbsJsonLd, breadcrumbsJsonLdScript, } from "./json-ld.js";
3
+ export { createBreadcrumbsT, BREADCRUMBS_MESSAGES_EN, } from "./i18n.js";
4
+ export { BREADCRUMBS_MESSAGES_SK } from "./i18n-sk.js";
@@ -0,0 +1,58 @@
1
+ import type { BreadcrumbItem } from "./Breadcrumbs.svelte";
2
+ /** Options for the JSON-LD helpers. */
3
+ export interface BreadcrumbsJsonLdOptions {
4
+ /**
5
+ * Base URL to resolve relative `href`s against (e.g. `page.url.origin` in
6
+ * SvelteKit). Google recommends absolute URLs in `BreadcrumbList` structured
7
+ * data; without a base, hrefs are emitted as-is.
8
+ */
9
+ baseUrl?: string | URL;
10
+ }
11
+ /** One `ListItem` of a schema.org `BreadcrumbList`. */
12
+ export interface BreadcrumbListItemJsonLd {
13
+ "@type": "ListItem";
14
+ /** 1-based position in the trail. */
15
+ position: number;
16
+ name: string;
17
+ /** The crumb's URL. Omitted for crumbs without `href` (typically the current page). */
18
+ item?: string;
19
+ }
20
+ /** A schema.org `BreadcrumbList` — the structured data search engines read. */
21
+ export interface BreadcrumbListJsonLd {
22
+ "@context": "https://schema.org";
23
+ "@type": "BreadcrumbList";
24
+ itemListElement: BreadcrumbListItemJsonLd[];
25
+ }
26
+ /**
27
+ * Builds a schema.org `BreadcrumbList` object from `Breadcrumbs` items — the
28
+ * structured data Google et al. use to render breadcrumb trails in search results.
29
+ *
30
+ * Crumbs without `href` (typically the last one, the current page) are emitted
31
+ * without `item`, which is exactly what Google's guidelines prescribe for the
32
+ * final breadcrumb.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * breadcrumbsJsonLd(items, { baseUrl: "https://example.com" });
37
+ * // { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [...] }
38
+ * ```
39
+ */
40
+ export declare function breadcrumbsJsonLd(items: BreadcrumbItem[], options?: BreadcrumbsJsonLdOptions): BreadcrumbListJsonLd;
41
+ /**
42
+ * Renders the full `<script type="application/ld+json">…</script>` tag as a string,
43
+ * safe to inject via `{@html}` (angle brackets and ampersands inside the JSON are
44
+ * `\uXXXX`-escaped, so a label can never break out of the script element).
45
+ *
46
+ * In SvelteKit, put it in the head:
47
+ *
48
+ * @example
49
+ * ```svelte
50
+ * <svelte:head>
51
+ * {@html breadcrumbsJsonLdScript(items, { baseUrl: page.url.origin })}
52
+ * </svelte:head>
53
+ * ```
54
+ *
55
+ * Or let the component render it inline via its `jsonLd` prop (search engines read
56
+ * JSON-LD anywhere in the document).
57
+ */
58
+ export declare function breadcrumbsJsonLdScript(items: BreadcrumbItem[], options?: BreadcrumbsJsonLdOptions): string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Builds a schema.org `BreadcrumbList` object from `Breadcrumbs` items — the
3
+ * structured data Google et al. use to render breadcrumb trails in search results.
4
+ *
5
+ * Crumbs without `href` (typically the last one, the current page) are emitted
6
+ * without `item`, which is exactly what Google's guidelines prescribe for the
7
+ * final breadcrumb.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * breadcrumbsJsonLd(items, { baseUrl: "https://example.com" });
12
+ * // { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [...] }
13
+ * ```
14
+ */
15
+ export function breadcrumbsJsonLd(items, options = {}) {
16
+ const { baseUrl } = options;
17
+ return {
18
+ "@context": "https://schema.org",
19
+ "@type": "BreadcrumbList",
20
+ itemListElement: items.map((item, i) => {
21
+ const out = {
22
+ "@type": "ListItem",
23
+ position: i + 1,
24
+ name: item.label,
25
+ };
26
+ if (item.href) {
27
+ out.item = baseUrl ? new URL(item.href, baseUrl).toString() : item.href;
28
+ }
29
+ return out;
30
+ }),
31
+ };
32
+ }
33
+ /**
34
+ * Renders the full `<script type="application/ld+json">…</script>` tag as a string,
35
+ * safe to inject via `{@html}` (angle brackets and ampersands inside the JSON are
36
+ * `\uXXXX`-escaped, so a label can never break out of the script element).
37
+ *
38
+ * In SvelteKit, put it in the head:
39
+ *
40
+ * @example
41
+ * ```svelte
42
+ * <svelte:head>
43
+ * {@html breadcrumbsJsonLdScript(items, { baseUrl: page.url.origin })}
44
+ * </svelte:head>
45
+ * ```
46
+ *
47
+ * Or let the component render it inline via its `jsonLd` prop (search engines read
48
+ * JSON-LD anywhere in the document).
49
+ */
50
+ export function breadcrumbsJsonLdScript(items, options = {}) {
51
+ const json = JSON.stringify(breadcrumbsJsonLd(items, options))
52
+ .replaceAll("&", "\\u0026")
53
+ .replaceAll("<", "\\u003c")
54
+ .replaceAll(">", "\\u003e");
55
+ return `<script type="application/ld+json">${json}</` + `script>`;
56
+ }
@@ -2,10 +2,21 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import { twMerge } from "../../utils/tw-merge.js";
4
4
  import { tooltip } from "../../actions/index.js";
5
+ import { autoHeight } from "../../attachments/index.js";
6
+ import { iconChevronDown } from "../../icons/index.js";
5
7
  import { isPlainObject } from "../../utils/is-plain-object.js";
6
8
  import { replaceMap } from "../../utils/replace-map.js";
7
9
  import type { TranslateFn } from "../../types.js";
8
10
 
11
+ /**
12
+ * Vertical alignment of the toggle button relative to the content:
13
+ * - `"bottom"` — always next to the last (clamped) line. Default, historical behavior.
14
+ * - `"top"` — always at the top of the content, in both states.
15
+ * - `"top-when-expanded"` — bottom while collapsed, top once expanded, so the toggle
16
+ * does not run away down a long expanded block.
17
+ */
18
+ export type CollapsibleToggleAlign = "bottom" | "top" | "top-when-expanded";
19
+
9
20
  export interface Props {
10
21
  /** Content to display */
11
22
  children: Snippet;
@@ -13,10 +24,29 @@
13
24
  lines?: number;
14
25
  /** Expanded state (bindable) */
15
26
  expanded?: boolean;
16
- /** Collapsed indicator character (default: "↓") */
27
+ /**
28
+ * Collapsed indicator character. Omit both indicators (the default) to get the
29
+ * rotating chevron; providing either one switches to plain text indicators
30
+ * (the omitted one falls back to the legacy "↓"/"↑" arrow).
31
+ */
17
32
  collapsedIndicator?: string;
18
- /** Expanded indicator character (default: "↑") */
33
+ /**
34
+ * Expanded indicator character. Omit both indicators (the default) to get the
35
+ * rotating chevron; providing either one switches to plain text indicators
36
+ * (the omitted one falls back to the legacy "↓"/"↑" arrow).
37
+ */
19
38
  expandedIndicator?: string;
39
+ /**
40
+ * Opt-in: smoothly animate the height between the collapsed and expanded state
41
+ * instead of snapping. Respects `prefers-reduced-motion` (snaps when reduce is
42
+ * set). Default: false.
43
+ */
44
+ animate?: boolean;
45
+ /**
46
+ * Opt-in: where to vertically align the toggle button. Default: `"bottom"`
47
+ * (always next to the last clamped line — the historical behavior).
48
+ */
49
+ toggleAlign?: CollapsibleToggleAlign;
20
50
  /** Container class */
21
51
  class?: string;
22
52
  /** Content wrapper class */
@@ -53,8 +83,10 @@
53
83
  children,
54
84
  lines = 1,
55
85
  expanded = $bindable(false),
56
- collapsedIndicator = "↓",
57
- expandedIndicator = "↑",
86
+ collapsedIndicator,
87
+ expandedIndicator,
88
+ animate = false,
89
+ toggleAlign = "bottom",
58
90
  class: classProp,
59
91
  classContent,
60
92
  classToggle,
@@ -80,21 +112,51 @@
80
112
  const l = Math.abs(lines);
81
113
  return l > 10 ? 10 : l;
82
114
  });
115
+
116
+ // The rotating chevron is the default indicator. Passing either custom indicator
117
+ // opts back into plain text; the unspecified one keeps its historical arrow.
118
+ let useChevron = $derived(
119
+ collapsedIndicator === undefined && expandedIndicator === undefined
120
+ );
121
+ let textIndicator = $derived(
122
+ expanded ? (expandedIndicator ?? "↑") : (collapsedIndicator ?? "↓")
123
+ );
124
+
125
+ let label = $derived(expanded ? t("less") : t("more"));
126
+
127
+ let _align = $derived(
128
+ toggleAlign === "top" || (toggleAlign === "top-when-expanded" && expanded)
129
+ ? "items-start"
130
+ : "items-end"
131
+ );
83
132
  </script>
84
133
 
134
+ {#snippet content()}
135
+ <div
136
+ bind:this={contentEl}
137
+ class={twMerge("flex-1", !expanded && `line-clamp-${_lines}`, classContent)}
138
+ >
139
+ {@render children()}
140
+ </div>
141
+ {/snippet}
142
+
85
143
  <div
86
144
  bind:this={el}
87
145
  bind:clientWidth={containerWidth}
88
146
  class={twMerge("stuic-collapsible", classProp)}
89
147
  {style}
90
148
  >
91
- <div class="flex items-end">
92
- <div
93
- bind:this={contentEl}
94
- class={twMerge("flex-1", !expanded && `line-clamp-${_lines}`, classContent)}
95
- >
96
- {@render children()}
97
- </div>
149
+ <div class={twMerge("flex", _align)}>
150
+ {#if animate}
151
+ <!-- Height-animated viewport: `autoHeight` drives this box to the natural
152
+ height of the (un)clamped content and clips while it transits. Only
153
+ rendered when the opt-in is active, so the default output is unchanged. -->
154
+ <div class="stuic-collapsible-viewport flex-1" {@attach autoHeight}>
155
+ {@render content()}
156
+ </div>
157
+ {:else}
158
+ {@render content()}
159
+ {/if}
98
160
  {#if needsCollapse}
99
161
  <button
100
162
  type="button"
@@ -102,12 +164,19 @@
102
164
  "stuic-collapsible-toggle cursor-pointer -my-1 -mr-2",
103
165
  classToggle
104
166
  )}
167
+ aria-expanded={expanded}
168
+ aria-label={label}
105
169
  onclick={() => (expanded = !expanded)}
106
- use:tooltip={() => ({
107
- content: expanded ? t("less") : t("more"),
108
- })}
170
+ use:tooltip={() => ({ content: label })}
109
171
  >
110
- {expanded ? expandedIndicator : collapsedIndicator}
172
+ {#if useChevron}
173
+ {@html iconChevronDown({
174
+ class: "stuic-collapsible-chevron",
175
+ "aria-hidden": "true",
176
+ })}
177
+ {:else}
178
+ {textIndicator}
179
+ {/if}
111
180
  </button>
112
181
  {/if}
113
182
  </div>
@@ -1,5 +1,13 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { TranslateFn } from "../../types.js";
3
+ /**
4
+ * Vertical alignment of the toggle button relative to the content:
5
+ * - `"bottom"` — always next to the last (clamped) line. Default, historical behavior.
6
+ * - `"top"` — always at the top of the content, in both states.
7
+ * - `"top-when-expanded"` — bottom while collapsed, top once expanded, so the toggle
8
+ * does not run away down a long expanded block.
9
+ */
10
+ export type CollapsibleToggleAlign = "bottom" | "top" | "top-when-expanded";
3
11
  export interface Props {
4
12
  /** Content to display */
5
13
  children: Snippet;
@@ -7,10 +15,29 @@ export interface Props {
7
15
  lines?: number;
8
16
  /** Expanded state (bindable) */
9
17
  expanded?: boolean;
10
- /** Collapsed indicator character (default: "↓") */
18
+ /**
19
+ * Collapsed indicator character. Omit both indicators (the default) to get the
20
+ * rotating chevron; providing either one switches to plain text indicators
21
+ * (the omitted one falls back to the legacy "↓"/"↑" arrow).
22
+ */
11
23
  collapsedIndicator?: string;
12
- /** Expanded indicator character (default: "↑") */
24
+ /**
25
+ * Expanded indicator character. Omit both indicators (the default) to get the
26
+ * rotating chevron; providing either one switches to plain text indicators
27
+ * (the omitted one falls back to the legacy "↓"/"↑" arrow).
28
+ */
13
29
  expandedIndicator?: string;
30
+ /**
31
+ * Opt-in: smoothly animate the height between the collapsed and expanded state
32
+ * instead of snapping. Respects `prefers-reduced-motion` (snaps when reduce is
33
+ * set). Default: false.
34
+ */
35
+ animate?: boolean;
36
+ /**
37
+ * Opt-in: where to vertically align the toggle button. Default: `"bottom"`
38
+ * (always next to the last clamped line — the historical behavior).
39
+ */
40
+ toggleAlign?: CollapsibleToggleAlign;
14
41
  /** Container class */
15
42
  class?: string;
16
43
  /** Content wrapper class */
@@ -2,21 +2,25 @@
2
2
 
3
3
  A component that truncates content to a specified number of lines with an expand/collapse toggle. Automatically detects if truncation is needed and only shows the toggle when content overflows.
4
4
 
5
+ The toggle renders a **rotating chevron** by default (points down when collapsed, rotates 180° to point up when expanded). Height changes snap by default; pass `animate` to transition them instead.
6
+
5
7
  ## Props
6
8
 
7
- | Prop | Type | Default | Description |
8
- | -------------------- | ---------------- | ------- | ------------------------------------------ |
9
- | `children` | `Snippet` | - | Content to display |
10
- | `lines` | `number` | `1` | Number of lines to show when collapsed |
11
- | `expanded` | `boolean` | `false` | Expanded state (bindable) |
12
- | `collapsedIndicator` | `string` | `"↓"` | Character/text shown when collapsed |
13
- | `expandedIndicator` | `string` | `"↑"` | Character/text shown when expanded |
14
- | `class` | `string` | - | Container element class |
15
- | `classContent` | `string` | - | Content wrapper class |
16
- | `classToggle` | `string` | - | Toggle button class |
17
- | `style` | `string` | - | Inline styles (for CSS variable overrides) |
18
- | `el` | `HTMLDivElement` | - | Bind reference to container element |
19
- | `t` | `TranslateFn` | - | Optional translate function |
9
+ | Prop | Type | Default | Description |
10
+ | -------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
11
+ | `children` | `Snippet` | - | Content to display |
12
+ | `lines` | `number` | `1` | Number of lines to show when collapsed |
13
+ | `expanded` | `boolean` | `false` | Expanded state (bindable) |
14
+ | `collapsedIndicator` | `string` | - | Text shown when collapsed. Opts **out** of the chevron (see below) |
15
+ | `expandedIndicator` | `string` | - | Text shown when expanded. Opts **out** of the chevron (see below) |
16
+ | `animate` | `boolean` | `false` | Opt-in: smoothly animate the height between collapsed/expanded. Respects `prefers-reduced-motion` |
17
+ | `toggleAlign` | `ToggleAlign` | `"bottom"` | Vertical alignment of the toggle button — `"bottom"` \| `"top"` \| `"top-when-expanded"` (see below) |
18
+ | `class` | `string` | - | Container element class |
19
+ | `classContent` | `string` | - | Content wrapper class |
20
+ | `classToggle` | `string` | - | Toggle button class |
21
+ | `style` | `string` | - | Inline styles (for CSS variable overrides) |
22
+ | `el` | `HTMLDivElement` | - | Bind reference to container element |
23
+ | `t` | `TranslateFn` | - | Optional translate function |
20
24
 
21
25
  ## Usage
22
26
 
@@ -42,9 +46,53 @@ A component that truncates content to a specified number of lines with an expand
42
46
  </Collapsible>
43
47
  ```
44
48
 
45
- ### Custom Indicators
49
+ ### Animated Height (opt-in)
50
+
51
+ ```svelte
52
+ <Collapsible lines={2} animate>
53
+ Expanding and collapsing transitions the height instead of snapping.
54
+ </Collapsible>
55
+ ```
56
+
57
+ Nothing animates unless you ask for it. With `animate`, the content is wrapped in a
58
+ `.stuic-collapsible-viewport` element whose height is driven to the natural height of the
59
+ (un)clamped content and transitioned; tune the duration with
60
+ `--stuic-collapsible-transition`. Under `prefers-reduced-motion: reduce` the height snaps,
61
+ exactly as it does without the prop.
62
+
63
+ ### Toggle Alignment
64
+
65
+ `toggleAlign` (`CollapsibleToggleAlign`) controls where the toggle button sits vertically:
66
+
67
+ | Value | Collapsed | Expanded |
68
+ | --------------------- | --------- | -------- |
69
+ | `"bottom"` (default) | bottom | bottom |
70
+ | `"top"` | top | top |
71
+ | `"top-when-expanded"` | bottom | top |
46
72
 
47
73
  ```svelte
74
+ <!-- default: always next to the last clamped line -->
75
+ <Collapsible lines={2}>Content.</Collapsible>
76
+
77
+ <!-- always at the top, e.g. next to a heading-like first line -->
78
+ <Collapsible lines={2} toggleAlign="top">Content.</Collapsible>
79
+
80
+ <!-- bottom while collapsed, top once expanded — so the toggle does not run away
81
+ down a long expanded paragraph -->
82
+ <Collapsible lines={2} toggleAlign="top-when-expanded">Content.</Collapsible>
83
+ ```
84
+
85
+ ### Indicators
86
+
87
+ The default indicator is a chevron that rotates 180° between the two states. Passing
88
+ **either** `collapsedIndicator` or `expandedIndicator` switches to plain text indicators;
89
+ the one you omit falls back to its legacy arrow (`↓` / `↑`).
90
+
91
+ ```svelte
92
+ <!-- rotating chevron (default) -->
93
+ <Collapsible>Content.</Collapsible>
94
+
95
+ <!-- plain text indicators -->
48
96
  <Collapsible collapsedIndicator="▼" expandedIndicator="▲">
49
97
  Content with custom expand/collapse indicators.
50
98
  </Collapsible>
@@ -89,15 +137,17 @@ A component that truncates content to a specified number of lines with an expand
89
137
 
90
138
  Override to customize appearance:
91
139
 
92
- | Variable | Default | Description |
93
- | ------------------------------------------ | -------------------------- | --------------------- |
94
- | `--stuic-collapsible-toggle-opacity` | `0.7` | Toggle button opacity |
95
- | `--stuic-collapsible-toggle-opacity-hover` | `1` | Hover opacity |
96
- | `--stuic-collapsible-toggle-padding-x` | `calc(var(--spacing) * 2)` | Horizontal padding |
97
- | `--stuic-collapsible-toggle-padding-y` | `0.25rem` | Vertical padding |
98
- | `--stuic-collapsible-transition` | `150ms` | Transition duration |
99
- | `--stuic-collapsible-ring-width` | `2px` | Focus ring width |
100
- | `--stuic-collapsible-ring-color` | `--stuic-color-ring` | Focus ring color |
140
+ | Variable | Default | Description |
141
+ | ------------------------------------------ | -------------------------- | ----------------------------------------------------------------- |
142
+ | `--stuic-collapsible-toggle-opacity` | `0.7` | Toggle button opacity |
143
+ | `--stuic-collapsible-toggle-opacity-hover` | `1` | Hover opacity |
144
+ | `--stuic-collapsible-toggle-padding-x` | `calc(var(--spacing) * 2)` | Horizontal padding |
145
+ | `--stuic-collapsible-toggle-padding-y` | `0.25rem` | Vertical padding |
146
+ | `--stuic-collapsible-chevron-size` | `16px` | Chevron width/height |
147
+ | `--stuic-collapsible-chevron-color` | `currentColor` | Chevron color |
148
+ | `--stuic-collapsible-transition` | `150ms` | Transition duration (opacity, chevron rotation, `animate` height) |
149
+ | `--stuic-collapsible-ring-width` | `2px` | Focus ring width |
150
+ | `--stuic-collapsible-ring-color` | `--stuic-color-ring` | Focus ring color |
101
151
 
102
152
  ### Global Override
103
153
 
@@ -107,3 +157,12 @@ Override to customize appearance:
107
157
  --stuic-collapsible-toggle-opacity-hover: 0.8;
108
158
  }
109
159
  ```
160
+
161
+ ## Accessibility
162
+
163
+ - The toggle is a real `<button>` carrying `aria-expanded`, which is also the hook the CSS
164
+ uses to rotate the chevron.
165
+ - The chevron SVG is `aria-hidden`; the button's accessible name comes from `aria-label`
166
+ (`t("more")` / `t("less")`), so it is announced even in chevron mode.
167
+ - Motion (chevron rotation and the opt-in height transition) is gated behind
168
+ `prefers-reduced-motion: no-preference`.
@@ -11,6 +11,10 @@
11
11
  --stuic-collapsible-toggle-padding-x: calc(var(--spacing) * 2);
12
12
  --stuic-collapsible-toggle-padding-y: calc(var(--spacing) * 1);
13
13
 
14
+ /* Chevron indicator (default) */
15
+ --stuic-collapsible-chevron-size: 16px;
16
+ --stuic-collapsible-chevron-color: currentColor;
17
+
14
18
  /* Focus ring (uses theme token) */
15
19
  --stuic-collapsible-ring-width: 2px;
16
20
  --stuic-collapsible-ring-color: var(--stuic-color-ring);
@@ -36,4 +40,38 @@
36
40
  outline: var(--stuic-collapsible-ring-width) solid var(--stuic-collapsible-ring-color);
37
41
  outline-offset: 2px;
38
42
  }
43
+
44
+ /* ============================================================================
45
+ CHEVRON INDICATOR (default)
46
+ ============================================================================ */
47
+
48
+ .stuic-collapsible-chevron {
49
+ display: block;
50
+ flex-shrink: 0;
51
+ width: var(--stuic-collapsible-chevron-size);
52
+ height: var(--stuic-collapsible-chevron-size);
53
+ color: var(--stuic-collapsible-chevron-color);
54
+ }
55
+
56
+ .stuic-collapsible-toggle[aria-expanded="true"] .stuic-collapsible-chevron {
57
+ transform: rotate(180deg);
58
+ }
59
+
60
+ /* ============================================================================
61
+ HEIGHT-ANIMATED VIEWPORT (opt-in via the `animate` prop)
62
+
63
+ The `autoHeight` attachment owns this element's inline `height` (and the
64
+ `overflow: clip` it applies *only* while that height transitions), so the
65
+ transition below is the entire opt-in. Without it the height simply snaps.
66
+ ============================================================================ */
67
+
68
+ @media (prefers-reduced-motion: no-preference) {
69
+ .stuic-collapsible-viewport {
70
+ transition: height var(--stuic-collapsible-transition, var(--stuic-transition));
71
+ }
72
+
73
+ .stuic-collapsible-chevron {
74
+ transition: transform var(--stuic-collapsible-transition, var(--stuic-transition));
75
+ }
76
+ }
39
77
  }
@@ -1 +1 @@
1
- export { default as Collapsible, type Props as CollapsibleProps, } from "./Collapsible.svelte";
1
+ export { default as Collapsible, type Props as CollapsibleProps, type CollapsibleToggleAlign, } from "./Collapsible.svelte";