@marianmeres/stuic 3.72.2 → 3.74.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 (31) hide show
  1. package/AGENTS.md +2 -2
  2. package/API.md +30 -0
  3. package/README.md +1 -1
  4. package/dist/components/Book/BookResponsive.svelte +2 -1
  5. package/dist/components/Input/FieldPhoneNumber.svelte +2 -3
  6. package/dist/components/LoginForm/LoginForm.svelte +26 -64
  7. package/dist/components/LoginForm/LoginForm.svelte.d.ts +0 -1
  8. package/dist/components/LoginForm/LoginFormModal.svelte +1 -2
  9. package/dist/components/LoginForm/index.css +12 -29
  10. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +9 -1
  11. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +6 -1
  12. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +8 -0
  13. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +5 -0
  14. package/dist/components/LoginOrRegisterForm/index.css +3 -3
  15. package/dist/components/Pill/Pill.svelte +205 -0
  16. package/dist/components/Pill/Pill.svelte.d.ts +51 -0
  17. package/dist/components/Pill/README.md +211 -0
  18. package/dist/components/Pill/index.css +488 -0
  19. package/dist/components/Pill/index.d.ts +1 -0
  20. package/dist/components/Pill/index.js +1 -0
  21. package/dist/components/PricingTable/PricingTable.svelte +0 -2
  22. package/dist/components/RegisterForm/RegisterForm.svelte +1 -23
  23. package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +0 -1
  24. package/dist/components/RegisterForm/RegisterFormModal.svelte +0 -1
  25. package/dist/components/RegisterForm/index.css +6 -16
  26. package/dist/index.css +1 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/mcp.js +1 -0
  30. package/docs/domains/components.md +2 -1
  31. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 55 UI components
26
+ ├── components/ # 56 UI components
27
27
  ├── actions/ # 15 Svelte actions
28
28
  ├── utils/ # 43 utility modules
29
29
  ├── themes/ # Generated theme CSS (css/) — definitions from @marianmeres/design-tokens
@@ -116,7 +116,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
116
116
 
117
117
  ### Domain Docs
118
118
 
119
- - [Components](./docs/domains/components.md) — 55 component directories, Props pattern, snippets
119
+ - [Components](./docs/domains/components.md) — 56 component directories, Props pattern, snippets
120
120
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
121
121
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
122
122
  - [Utils](./docs/domains/utils.md) — 43 utility modules
package/API.md CHANGED
@@ -841,6 +841,36 @@ User avatar with fallback to initials or icon.
841
841
  <!-- Shows "JD" initials -->
842
842
  ```
843
843
 
844
+ #### `Pill`
845
+
846
+ Inline rounded badge/tag/chip with intent + variant + size system. Polymorphic: renders as `<span>` (default), `<a>` (when `href` set), or `<button>` (when `onclick` set).
847
+
848
+ | Prop | Type | Default | Description |
849
+ | --------------- | ------------------------------------------------------------------ | -------- | ---------------------------------------------------- |
850
+ | `intent` | `"primary" \| "accent" \| "destructive" \| "warning" \| "success"` | - | Semantic color |
851
+ | `variant` | `"solid" \| "outline" \| "ghost" \| "soft" \| "link"` | `"soft"` | Visual treatment |
852
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Pill size |
853
+ | `roundedFull` | `boolean` | `true` | Fully rounded (9999px). `false` → element radius |
854
+ | `block` | `boolean` | `false` | Block-level flex (full width) |
855
+ | `dot` | `boolean` | `false` | Status dot before content |
856
+ | `dismissible` | `boolean` | `false` | Built-in X dismiss button |
857
+ | `ondismiss` | `(e: MouseEvent) => void` | - | Called when X clicked (stops propagation) |
858
+ | `active` | `boolean` | `false` | Selected state (filter-chip) |
859
+ | `muted` | `boolean` | `false` | Lower opacity |
860
+ | `href`, `target`| `string` | - | Render as `<a>` |
861
+ | `onclick` | `(e: MouseEvent) => void` | - | Render as `<button>` |
862
+ | `contentBefore` | `THC` | - | Content before children |
863
+ | `contentAfter` | `THC` | - | Content after children |
864
+
865
+ ```svelte
866
+ <Pill intent="success" dot>Online</Pill>
867
+ <Pill intent="primary" dismissible ondismiss={() => removeTag()}>tag</Pill>
868
+ <Pill intent="accent" href="/profile">Profile</Pill>
869
+ <Pill intent="warning" variant="outline" active onclick={toggle}>Filter</Pill>
870
+ ```
871
+
872
+ CSS tokens: `--stuic-pill-radius`, `--stuic-pill-font-family`, `--stuic-pill-font-weight`, `--stuic-pill-gap`, `--stuic-pill-dot-size`, `--stuic-pill-ring-{width,color}`, `--stuic-pill-{padding-x,padding-y,font-size,min-height}-{sm,md,lg}`.
873
+
844
874
  #### `KbdShortcut`
845
875
 
846
876
  Keyboard shortcut display.
package/README.md CHANGED
@@ -164,7 +164,7 @@ CommandMenu, DropdownMenu, TabbedMenu, TypeaheadInput, KbdShortcut
164
164
 
165
165
  ### Display & Utility
166
166
 
167
- Avatar, Book, BookResponsive, Card, Carousel, Circle, AnimatedElipsis, H, IconSwap, ImageCycler, Separator, ThemePreview, Tree, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, AssetsPreviewInline, DataTable
167
+ Avatar, Pill, Book, BookResponsive, Card, Carousel, Circle, AnimatedElipsis, H, IconSwap, ImageCycler, Separator, ThemePreview, Tree, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, AssetsPreviewInline, DataTable
168
168
 
169
169
  ### E-commerce
170
170
 
@@ -25,6 +25,7 @@
25
25
  </script>
26
26
 
27
27
  <script lang="ts">
28
+ import { untrack } from "svelte";
28
29
  import {
29
30
  iconBookOpen,
30
31
  iconArrowRight as iconNext,
@@ -89,7 +90,7 @@
89
90
 
90
91
  // ---- Manual mode override ----
91
92
 
92
- let manualMode: "book" | "inline" | null = $state(initialMode ?? null);
93
+ let manualMode: "book" | "inline" | null = $state(untrack(() => initialMode ?? null));
93
94
 
94
95
  // ---- Inline mode ----
95
96
 
@@ -59,7 +59,7 @@
59
59
  </script>
60
60
 
61
61
  <script lang="ts">
62
- import { tick } from "svelte";
62
+ import { tick, untrack } from "svelte";
63
63
  import {
64
64
  validate as validateAction,
65
65
  type ValidationResult,
@@ -142,9 +142,8 @@
142
142
  });
143
143
 
144
144
  // Selected country object (initialize once from defaultCountry prop)
145
- const _initCountry = defaultCountry;
146
145
  let selectedCountry: Country | undefined = $state(
147
- _initCountry ? ISO_MAP.get(_initCountry.toUpperCase()) : undefined
146
+ untrack(() => (defaultCountry ? ISO_MAP.get(defaultCountry.toUpperCase()) : undefined))
148
147
  );
149
148
 
150
149
  // Internal local number (for controlled input)
@@ -73,8 +73,6 @@
73
73
  unstyled?: boolean;
74
74
  class?: string;
75
75
  el?: HTMLFormElement;
76
-
77
- compact?: boolean;
78
76
  }
79
77
  </script>
80
78
 
@@ -111,7 +109,6 @@
111
109
  unstyled = false,
112
110
  class: classProp,
113
111
  el = $bindable(),
114
- compact,
115
112
  ...rest
116
113
  }: Props = $props();
117
114
 
@@ -158,13 +155,7 @@
158
155
  let _class = $derived(unstyled ? classProp : twMerge("stuic-login-form", classProp));
159
156
  </script>
160
157
 
161
- <form
162
- bind:this={el}
163
- class={_class}
164
- use:onSubmitValidityCheck
165
- {...rest}
166
- data-compact={compact ? "" : undefined}
167
- >
158
+ <form bind:this={el} class={_class} use:onSubmitValidityCheck {...rest}>
168
159
  <!-- General error alert -->
169
160
  <DismissibleMessage message={error} intent="destructive" onDismiss={false} />
170
161
 
@@ -184,7 +175,6 @@
184
175
  required
185
176
  name="login-email"
186
177
  labelLeftBreakpoint={0}
187
- class={compact ? "mb-4" : ""}
188
178
  validate={{
189
179
  customValidator(val) {
190
180
  return fieldError("email") || "";
@@ -203,7 +193,6 @@
203
193
  required
204
194
  name="login-password"
205
195
  labelLeftBreakpoint={0}
206
- class={compact ? "mb-4" : ""}
207
196
  validate={{
208
197
  customValidator(val) {
209
198
  return fieldError("password") || "";
@@ -211,9 +200,22 @@
211
200
  }}
212
201
  />
213
202
 
214
- {#if compact}
215
- <!-- Compact: remember me + submit in one row -->
216
- <div class={unstyled ? undefined : "stuic-login-form-compact-cta"}>
203
+ <!-- CTA -->
204
+ {#if submitButton}
205
+ {@render submitButton({ isSubmitting, disabled: isSubmitting })}
206
+ {:else}
207
+ <div class={unstyled ? undefined : "stuic-login-form-submit"}>
208
+ <Button intent="primary" type="submit" disabled={isSubmitting} class="w-full">
209
+ {isSubmitting
210
+ ? (submittingLabel ?? t("login_form.submitting"))
211
+ : (submitLabel ?? t("login_form.submit"))}
212
+ </Button>
213
+ </div>
214
+ {/if}
215
+
216
+ <!-- Remember me + Forgot password -->
217
+ {#if showRememberMe || onForgotPassword}
218
+ <div class={unstyled ? undefined : "stuic-login-form-options"}>
217
219
  {#if showRememberMe}
218
220
  <!-- svelte-ignore binding_property_non_reactive -->
219
221
  <span use:tooltip aria-label={t("login_form.remember_me_tooltip")}>
@@ -225,58 +227,18 @@
225
227
  />
226
228
  </span>
227
229
  {/if}
228
- {#if submitButton}
229
- {@render submitButton({ isSubmitting, disabled: isSubmitting })}
230
- {:else}
231
- <Button intent="primary" type="submit" disabled={isSubmitting} class="block">
232
- {isSubmitting
233
- ? (submittingLabel ?? t("login_form.submitting"))
234
- : (submitLabel ?? t("login_form.submit"))}
230
+ {#if onForgotPassword}
231
+ <Button
232
+ variant="link"
233
+ type="button"
234
+ class={unstyled ? undefined : "text-muted-foreground ml-auto"}
235
+ size="sm"
236
+ onclick={onForgotPassword}
237
+ >
238
+ {t("login_form.forgot_password")}
235
239
  </Button>
236
240
  {/if}
237
241
  </div>
238
- {:else}
239
- <!-- Normal: remember me above submit -->
240
- {#if showRememberMe}
241
- <!-- svelte-ignore binding_property_non_reactive -->
242
- <div class={unstyled ? undefined : "stuic-login-form-remember"}>
243
- <span use:tooltip aria-label={t("login_form.remember_me_tooltip")}>
244
- <FieldCheckbox
245
- bind:checked={formData.rememberMe}
246
- label={t("login_form.remember_me")}
247
- name="login-remember-me"
248
- />
249
- </span>
250
- </div>
251
- {/if}
252
-
253
- <!-- CTA -->
254
- {#if submitButton}
255
- {@render submitButton({ isSubmitting, disabled: isSubmitting })}
256
- {:else}
257
- <div class={unstyled ? undefined : "stuic-login-form-submit"}>
258
- <Button intent="primary" type="submit" disabled={isSubmitting} class="w-full">
259
- {isSubmitting
260
- ? (submittingLabel ?? t("login_form.submitting"))
261
- : (submitLabel ?? t("login_form.submit"))}
262
- </Button>
263
- </div>
264
- {/if}
265
- {/if}
266
-
267
- <!-- Forgot password -->
268
- {#if onForgotPassword}
269
- <div class={unstyled ? undefined : "stuic-login-form-forgot"}>
270
- <Button
271
- variant="link"
272
- type="button"
273
- class="text-muted-foreground"
274
- size="sm"
275
- onclick={onForgotPassword}
276
- >
277
- {t("login_form.forgot_password")}
278
- </Button>
279
- </div>
280
242
  {/if}
281
243
 
282
244
  <!-- Social logins -->
@@ -57,7 +57,6 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
57
57
  unstyled?: boolean;
58
58
  class?: string;
59
59
  el?: HTMLFormElement;
60
- compact?: boolean;
61
60
  }
62
61
  declare const LoginForm: import("svelte").Component<Props, {}, "el" | "formData">;
63
62
  type LoginForm = ReturnType<typeof LoginForm>;
@@ -154,7 +154,7 @@
154
154
  classDialog="flex items-center justify-center"
155
155
  >
156
156
  {#snippet header()}
157
- <div class="flex items-center justify-between p-4 pb-0">
157
+ <div class="flex items-center justify-between p-4">
158
158
  <H level={1} renderLevel={3} class="pl-2">
159
159
  {title ?? t("login_form.modal_title")}
160
160
  </H>
@@ -193,7 +193,6 @@
193
193
  t={tProp}
194
194
  {unstyled}
195
195
  class={classForm}
196
- compact
197
196
  />
198
197
  </div>
199
198
  </Modal>
@@ -2,11 +2,9 @@
2
2
  /* LoginForm */
3
3
  --stuic-login-form-gap: 0rem;
4
4
  --stuic-login-form-gap-row: 1rem;
5
- --stuic-login-form-forgot-margin-y: 0.5rem;
6
- --stuic-login-form-forgot-margin-x: 0.5rem;
7
5
 
8
6
  /* Social login section */
9
- --stuic-login-form-social-margin-top: 1rem;
7
+ --stuic-login-form-social-margin-top: 1.5rem;
10
8
  --stuic-login-form-social-gap: 0.75rem;
11
9
  --stuic-login-form-social-divider-color: var(--stuic-color-muted-foreground);
12
10
  --stuic-login-form-social-divider-line-color: var(--stuic-color-border);
@@ -21,18 +19,15 @@
21
19
  gap: var(--stuic-login-form-gap);
22
20
  }
23
21
 
24
- .stuic-login-form-forgot {
25
- margin: var(--stuic-login-form-forgot-margin-y)
26
- var(--stuic-login-form-forgot-margin-x) var(--stuic-login-form-forgot-margin-y)
27
- var(--stuic-login-form-forgot-margin-x);
28
- }
29
-
30
- .stuic-login-form-remember {
31
- margin-top: 0.25rem;
22
+ .stuic-login-form-submit {
23
+ margin-top: 1.5rem;
24
+ margin-bottom: 1.5rem;
32
25
  }
33
26
 
34
- .stuic-login-form-submit {
35
- margin-top: 1rem;
27
+ .stuic-login-form-options {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 1rem;
36
31
  }
37
32
 
38
33
  /* Social login container */
@@ -63,21 +58,9 @@
63
58
  flex-direction: column;
64
59
  gap: var(--stuic-login-form-social-gap);
65
60
  }
61
+ }
66
62
 
67
- .stuic-login-form-compact-cta {
68
- display: flex;
69
- align-items: center;
70
- /* justify-content: space-between; */
71
- margin-top: 0.5rem;
72
- gap: 2rem;
73
- }
74
-
75
- .stuic-login-form[data-compact] {
76
- .stuic-login-form-submit {
77
- margin-top: 0.5rem;
78
- }
79
- .stuic-button {
80
- flex: 1;
81
- }
82
- }
63
+ /* Tighten default FieldInput bottom margin (mb-8 → 1rem) inside the form */
64
+ .stuic-login-form .stuic-input {
65
+ margin-bottom: 1rem;
83
66
  }
@@ -40,8 +40,14 @@
40
40
  /** Applied to both inner forms' submit buttons. */
41
41
  isSubmitting?: boolean;
42
42
 
43
+ /**
44
+ * Called when "Forgot password?" is clicked in login mode.
45
+ * If undefined, the link is not rendered.
46
+ */
47
+ onForgotPassword?: () => void;
48
+
43
49
  /** Pass-through props for the inner LoginForm (spread). */
44
- loginProps?: Omit<LoginFormProps, InnerPropsCommonOmit>;
50
+ loginProps?: Omit<LoginFormProps, InnerPropsCommonOmit | "onForgotPassword">;
45
51
 
46
52
  /** Pass-through props for the inner RegisterForm (spread). */
47
53
  registerProps?: Omit<RegisterFormProps, InnerPropsCommonOmit>;
@@ -137,6 +143,7 @@
137
143
  onVerify,
138
144
  onResendCode,
139
145
  isSubmitting = false,
146
+ onForgotPassword,
140
147
  loginProps,
141
148
  registerProps,
142
149
  verifyProps,
@@ -221,6 +228,7 @@
221
228
  onSubmit={onLogin}
222
229
  {isSubmitting}
223
230
  {notifications}
231
+ {onForgotPassword}
224
232
  t={tProp}
225
233
  {...loginProps}
226
234
  />
@@ -22,8 +22,13 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
22
22
  onRegister: (data: RegisterFormData) => void;
23
23
  /** Applied to both inner forms' submit buttons. */
24
24
  isSubmitting?: boolean;
25
+ /**
26
+ * Called when "Forgot password?" is clicked in login mode.
27
+ * If undefined, the link is not rendered.
28
+ */
29
+ onForgotPassword?: () => void;
25
30
  /** Pass-through props for the inner LoginForm (spread). */
26
- loginProps?: Omit<LoginFormProps, InnerPropsCommonOmit>;
31
+ loginProps?: Omit<LoginFormProps, InnerPropsCommonOmit | "onForgotPassword">;
27
32
  /** Pass-through props for the inner RegisterForm (spread). */
28
33
  registerProps?: Omit<RegisterFormProps, InnerPropsCommonOmit>;
29
34
  /**
@@ -33,6 +33,12 @@
33
33
 
34
34
  isSubmitting?: boolean;
35
35
 
36
+ /**
37
+ * Called when "Forgot password?" is clicked in login mode.
38
+ * If undefined, the link is not rendered.
39
+ */
40
+ onForgotPassword?: () => void;
41
+
36
42
  loginProps?: InnerProps["loginProps"];
37
43
  registerProps?: InnerProps["registerProps"];
38
44
  verifyProps?: InnerProps["verifyProps"];
@@ -101,6 +107,7 @@
101
107
  onVerify,
102
108
  onResendCode,
103
109
  isSubmitting = false,
110
+ onForgotPassword,
104
111
  loginProps,
105
112
  registerProps,
106
113
  verifyProps,
@@ -188,6 +195,7 @@
188
195
  {onVerify}
189
196
  {onResendCode}
190
197
  {isSubmitting}
198
+ {onForgotPassword}
191
199
  {loginProps}
192
200
  {registerProps}
193
201
  {verifyProps}
@@ -20,6 +20,11 @@ export interface Props {
20
20
  /** Called when the user clicks "Resend code" in the verify view. */
21
21
  onResendCode?: () => Promise<void> | void;
22
22
  isSubmitting?: boolean;
23
+ /**
24
+ * Called when "Forgot password?" is clicked in login mode.
25
+ * If undefined, the link is not rendered.
26
+ */
27
+ onForgotPassword?: () => void;
23
28
  loginProps?: InnerProps["loginProps"];
24
29
  registerProps?: InnerProps["registerProps"];
25
30
  verifyProps?: InnerProps["verifyProps"];
@@ -1,10 +1,10 @@
1
1
  :root {
2
2
  /* LoginOrRegisterForm */
3
- --stuic-login-or-register-form-gap: 1rem;
4
- --stuic-login-or-register-form-switcher-margin-bottom: 1rem;
3
+ --stuic-login-or-register-form-gap: 0rem;
4
+ --stuic-login-or-register-form-switcher-margin-bottom: 2rem;
5
5
 
6
6
  /* Social login section (shared, rendered at composite level) */
7
- --stuic-login-or-register-form-social-margin-top: 1rem;
7
+ --stuic-login-or-register-form-social-margin-top: 1.5rem;
8
8
  --stuic-login-or-register-form-social-gap: 0.75rem;
9
9
  --stuic-login-or-register-form-social-divider-color: var(
10
10
  --stuic-color-muted-foreground
@@ -0,0 +1,205 @@
1
+ <script lang="ts" module>
2
+ import type {
3
+ HTMLAttributes,
4
+ HTMLAnchorAttributes,
5
+ HTMLButtonAttributes,
6
+ } from "svelte/elements";
7
+ import type { Snippet } from "svelte";
8
+ import type { IntentColorKey } from "../../utils/design-tokens.js";
9
+ import type { THC } from "../Thc/Thc.svelte";
10
+
11
+ export type PillVariant = "solid" | "outline" | "ghost" | "soft" | "link";
12
+ export type PillSize = "sm" | "md" | "lg";
13
+
14
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
15
+ /** Color intent (semantic meaning) */
16
+ intent?: IntentColorKey;
17
+ /** Visual variant (how colors are applied) */
18
+ variant?: PillVariant | string;
19
+ /** Size preset */
20
+ size?: PillSize | string;
21
+ /** Reduce emphasis (lower opacity) */
22
+ muted?: boolean;
23
+ /** Selected/active state — useful for filter-chip behavior */
24
+ active?: boolean;
25
+ /** Pill is fully rounded by default; set false to use element radius */
26
+ roundedFull?: boolean;
27
+ /** Render as block-level flex (full width). Inline-flex by default. */
28
+ block?: boolean;
29
+ /** Skip all default styling, use only custom classes */
30
+ unstyled?: boolean;
31
+ /** Additional CSS classes */
32
+ class?: string;
33
+ /** Render as anchor tag */
34
+ href?: string;
35
+ /** Link target (e.g. "_blank") — only relevant when href is set */
36
+ target?: string;
37
+ /** Render as button (when href not set) */
38
+ onclick?: (e: MouseEvent) => void;
39
+ /** Disabled (interactive variants only) */
40
+ disabled?: boolean;
41
+ /** Show built-in X dismiss control */
42
+ dismissible?: boolean;
43
+ /** Called when X is clicked. Stops propagation so parent onclick is unaffected. */
44
+ ondismiss?: (e: MouseEvent) => void;
45
+ /** Status dot rendered before content (uses current intent color) */
46
+ dot?: boolean;
47
+ /** Content rendered before children */
48
+ contentBefore?: THC;
49
+ /** Content rendered after children */
50
+ contentAfter?: THC;
51
+ /** Bindable element reference */
52
+ el?: HTMLElement;
53
+ /** Content snippet */
54
+ children?: Snippet;
55
+ }
56
+ </script>
57
+
58
+ <script lang="ts">
59
+ import { twMerge } from "../../utils/tw-merge.js";
60
+ import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
61
+ import { X } from "../X/index.js";
62
+
63
+ let {
64
+ class: classProp,
65
+ intent,
66
+ variant = "soft",
67
+ size = "md",
68
+ muted = false,
69
+ active = false,
70
+ roundedFull = true,
71
+ block = false,
72
+ unstyled = false,
73
+ href,
74
+ target,
75
+ onclick,
76
+ disabled,
77
+ dismissible = false,
78
+ ondismiss,
79
+ dot = false,
80
+ contentBefore,
81
+ contentAfter,
82
+ el = $bindable(),
83
+ children,
84
+ ...rest
85
+ }: Props = $props();
86
+
87
+ let _class = $derived(unstyled ? classProp : twMerge("stuic-pill", classProp));
88
+
89
+ function handleDismiss(e: MouseEvent) {
90
+ e.stopPropagation();
91
+ ondismiss?.(e);
92
+ }
93
+ </script>
94
+
95
+ {#snippet body()}
96
+ {#if dot}
97
+ <span class="stuic-pill-dot" aria-hidden="true"></span>
98
+ {/if}
99
+ {#if isTHCNotEmpty(contentBefore)}
100
+ <Thc thc={contentBefore as THC} />
101
+ {/if}
102
+ {@render children?.()}
103
+ {#if isTHCNotEmpty(contentAfter)}
104
+ <Thc thc={contentAfter as THC} />
105
+ {/if}
106
+ {/snippet}
107
+
108
+ {#snippet dismissBtn()}
109
+ <button
110
+ type="button"
111
+ class="stuic-pill-dismiss"
112
+ aria-label="Dismiss"
113
+ onclick={handleDismiss}
114
+ {disabled}
115
+ >
116
+ <X strokeWidth={2} />
117
+ </button>
118
+ {/snippet}
119
+
120
+ {#if dismissible}
121
+ <!-- Wrapper pattern: outer span carries pill styling; inner element is the
122
+ interactive area (when href/onclick); X dismiss is a sibling button. -->
123
+ <span
124
+ bind:this={el}
125
+ class={_class}
126
+ data-intent={!unstyled ? intent : undefined}
127
+ data-variant={!unstyled ? variant : undefined}
128
+ data-size={!unstyled ? size : undefined}
129
+ data-muted={!unstyled && muted ? "true" : undefined}
130
+ data-active={!unstyled && active ? "true" : undefined}
131
+ data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
132
+ data-block={!unstyled && block ? "true" : undefined}
133
+ data-with-dot={!unstyled && dot ? "true" : undefined}
134
+ data-dismissible="true"
135
+ {...rest}
136
+ >
137
+ {#if href}
138
+ <a {href} {target} class="stuic-pill-main">
139
+ {@render body()}
140
+ </a>
141
+ {:else if onclick}
142
+ <button type="button" class="stuic-pill-main" {onclick} {disabled}>
143
+ {@render body()}
144
+ </button>
145
+ {:else}
146
+ {@render body()}
147
+ {/if}
148
+ {@render dismissBtn()}
149
+ </span>
150
+ {:else if href}
151
+ <a
152
+ {href}
153
+ {target}
154
+ bind:this={el}
155
+ class={_class}
156
+ data-intent={!unstyled ? intent : undefined}
157
+ data-variant={!unstyled ? variant : undefined}
158
+ data-size={!unstyled ? size : undefined}
159
+ data-muted={!unstyled && muted ? "true" : undefined}
160
+ data-active={!unstyled && active ? "true" : undefined}
161
+ data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
162
+ data-block={!unstyled && block ? "true" : undefined}
163
+ data-with-dot={!unstyled && dot ? "true" : undefined}
164
+ data-interactive="true"
165
+ {...rest as HTMLAnchorAttributes}
166
+ >
167
+ {@render body()}
168
+ </a>
169
+ {:else if onclick}
170
+ <button
171
+ type="button"
172
+ bind:this={el}
173
+ class={_class}
174
+ data-intent={!unstyled ? intent : undefined}
175
+ data-variant={!unstyled ? variant : undefined}
176
+ data-size={!unstyled ? size : undefined}
177
+ data-muted={!unstyled && muted ? "true" : undefined}
178
+ data-active={!unstyled && active ? "true" : undefined}
179
+ data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
180
+ data-block={!unstyled && block ? "true" : undefined}
181
+ data-with-dot={!unstyled && dot ? "true" : undefined}
182
+ data-interactive="true"
183
+ {onclick}
184
+ {disabled}
185
+ {...rest as HTMLButtonAttributes}
186
+ >
187
+ {@render body()}
188
+ </button>
189
+ {:else}
190
+ <span
191
+ bind:this={el}
192
+ class={_class}
193
+ data-intent={!unstyled ? intent : undefined}
194
+ data-variant={!unstyled ? variant : undefined}
195
+ data-size={!unstyled ? size : undefined}
196
+ data-muted={!unstyled && muted ? "true" : undefined}
197
+ data-active={!unstyled && active ? "true" : undefined}
198
+ data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
199
+ data-block={!unstyled && block ? "true" : undefined}
200
+ data-with-dot={!unstyled && dot ? "true" : undefined}
201
+ {...rest}
202
+ >
203
+ {@render body()}
204
+ </span>
205
+ {/if}