@marianmeres/stuic 3.104.0 → 3.106.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.
@@ -173,6 +173,15 @@ export function validate(el, fn) {
173
173
  const _doValidate = () => {
174
174
  if (!enabled)
175
175
  return;
176
+ // A focused, dirty field torn down by a route change fires a final
177
+ // synchronous `change`/`blur` while being removed from the DOM. That
178
+ // removal runs inside Svelte's flush, so writing `validation` state here
179
+ // would throw `state_unsafe_mutation`. By then the node is already
180
+ // detached (`isConnected === false`) and the field is going away — skip.
181
+ // No-op for normal interactive validation and for the synchronous
182
+ // imperative `validate()` path, both of which run while connected.
183
+ if (!el.isConnected)
184
+ return;
176
185
  el.checkValidity();
177
186
  // Store customValidator message directly - hidden inputs (type="hidden")
178
187
  // don't populate el.validationMessage even when setCustomValidity() is called.
@@ -210,9 +219,6 @@ export function validate(el, fn) {
210
219
  return m;
211
220
  }, []);
212
221
  // console.log(1111, validityState, el);
213
- // hm... Uncaught Svelte error: state_unsafe_mutation...
214
- // the `tick` await helps, but I'm not really sure I understand the internals...
215
- // tick().then(() => {
216
222
  setValidationResult?.({
217
223
  validity: validityState,
218
224
  reasons,
@@ -224,7 +230,6 @@ export function validate(el, fn) {
224
230
  el.validationMessage ||
225
231
  "This field is invalid. Please review and try again."),
226
232
  });
227
- // });
228
233
  };
229
234
  // Expose the current validator to the host so it can trigger validation
230
235
  // imperatively (e.g., on submit). The closure captures the current
@@ -32,6 +32,14 @@
32
32
  unstyled?: boolean;
33
33
  /** Render as rounded-full */
34
34
  roundedFull?: boolean;
35
+ /**
36
+ * Trim the icon-side horizontal padding down to the vertical padding, so a
37
+ * leading/trailing icon sits the same distance from the edge as a rounded icon
38
+ * (aspect1) button. Pair with `roundedFull` for the "rounded icon button with
39
+ * label" pill look (e.g. a "Back" button: prev arrow + label). Size-aware and
40
+ * RTL-aware (`leading` = start side, `trailing` = end side).
41
+ */
42
+ iconEdge?: "leading" | "trailing";
35
43
  /** Render as aspect ratio 1 */
36
44
  aspect1?: boolean;
37
45
  /** Icon-only button (implies aspect1, adds data-icon-button for global CSS targeting) */
@@ -104,6 +112,7 @@
104
112
  raised = false,
105
113
  unstyled = false,
106
114
  roundedFull = false,
115
+ iconEdge,
107
116
  aspect1 = false,
108
117
  iconButton = false,
109
118
  tooltip: _tooltip,
@@ -168,6 +177,7 @@
168
177
  data-raised={!unstyled && raised ? "true" : undefined}
169
178
  data-checked={roleSwitch && checked ? "true" : undefined}
170
179
  data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
180
+ data-icon-edge={!unstyled && iconEdge ? iconEdge : undefined}
171
181
  data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
172
182
  data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
173
183
  data-x={!unstyled && !!_xProps ? "true" : undefined}
@@ -213,6 +223,7 @@
213
223
  data-raised={!unstyled && raised ? "true" : undefined}
214
224
  data-checked={roleSwitch && checked ? "true" : undefined}
215
225
  data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
226
+ data-icon-edge={!unstyled && iconEdge ? iconEdge : undefined}
216
227
  data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
217
228
  data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
218
229
  data-x={!unstyled && !!_xProps ? "true" : undefined}
@@ -27,6 +27,14 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
27
27
  unstyled?: boolean;
28
28
  /** Render as rounded-full */
29
29
  roundedFull?: boolean;
30
+ /**
31
+ * Trim the icon-side horizontal padding down to the vertical padding, so a
32
+ * leading/trailing icon sits the same distance from the edge as a rounded icon
33
+ * (aspect1) button. Pair with `roundedFull` for the "rounded icon button with
34
+ * label" pill look (e.g. a "Back" button: prev arrow + label). Size-aware and
35
+ * RTL-aware (`leading` = start side, `trailing` = end side).
36
+ */
37
+ iconEdge?: "leading" | "trailing";
30
38
  /** Render as aspect ratio 1 */
31
39
  aspect1?: boolean;
32
40
  /** Icon-only button (implies aspect1, adds data-icon-button for global CSS targeting) */
@@ -20,6 +20,7 @@ A flexible button component with semantic intents, visual variants, sizes, and o
20
20
  | `iconSwap` | `[string \| Snippet, string \| Snippet]` | - | Two icon states with swap animation (implies iconButton) |
21
21
  | `x` | `boolean \| XProps` | - | Normalized "X" icon button shortcut (close/dismiss) |
22
22
  | `nav` | `"prev" \| "next" \| ButtonNavProps` | - | Normalized prev/next icon button shortcut (arrow by default; `x` wins on conflict) |
23
+ | `iconEdge` | `"leading" \| "trailing"` | - | Trim icon-side padding to the y-padding (pill + edge-flush icon; pair with `roundedFull`) |
23
24
  | `class` | `string` | - | Additional CSS classes |
24
25
 
25
26
  ## Snippet Props
@@ -134,6 +135,26 @@ Global CSS targeting for all icon buttons:
134
135
  }
135
136
  ```
136
137
 
138
+ ### Pill with edge-flush icon (`iconEdge`)
139
+
140
+ The "rounded icon button with label" look: a pill-shaped button whose leading (or
141
+ trailing) icon sits the same distance from the edge as a rounded icon (nav) button.
142
+ `iconEdge` trims the icon-side horizontal padding down to the vertical padding — it's
143
+ size-aware and uses logical properties, so `leading`/`trailing` follow text direction
144
+ (RTL-safe). It does NOT round the button on its own; pair it with `roundedFull`.
145
+
146
+ ```svelte
147
+ <Button roundedFull iconEdge="leading">{@html iconArrowLeft({ size: 24 })} Back</Button>
148
+ <Button roundedFull iconEdge="trailing">Next {@html iconArrowRight({ size: 24 })}</Button>
149
+ ```
150
+
151
+ Notes:
152
+
153
+ - `iconEdge` is composable — it only trims padding, so it also works on a
154
+ default-radius (non-pill) button if you want the icon flush without the pill shape.
155
+ - You compose the icon + label yourself in `children` (no default icon, unlike `nav`),
156
+ so it works with any icon. Use `size: 24` to match the nav button's arrow.
157
+
137
158
  ### Custom Styling
138
159
 
139
160
  ```svelte
@@ -199,6 +220,7 @@ The component uses data attributes for styling:
199
220
  - `data-raised` - Present when raised
200
221
  - `data-checked` - Present when roleSwitch is enabled and checked
201
222
  - `data-rounded-full` - Present when roundedFull
223
+ - `data-icon-edge` - Set to `"leading"` or `"trailing"` when iconEdge is set
202
224
  - `data-aspect1` - Present when aspect1 (or iconButton, or x, or nav)
203
225
  - `data-icon-button` - Present when iconButton (or x, or nav)
204
226
  - `data-x` - Present when x is set
@@ -422,6 +422,38 @@
422
422
  padding: var(--stuic-button-padding-y-xl);
423
423
  }
424
424
 
425
+ /* ============================================================================
426
+ ICON EDGE
427
+ Trim the icon-side horizontal padding down to the vertical padding, so a
428
+ leading/trailing icon sits the same distance from the edge as a rounded icon
429
+ (aspect1) button. Pair with [data-rounded-full] for the "rounded icon button
430
+ with label" pill look. Logical props => leading/trailing follow text direction.
431
+ ============================================================================ */
432
+ .stuic-button[data-icon-edge="leading"][data-size="sm"] {
433
+ padding-inline-start: var(--stuic-button-padding-y-sm);
434
+ }
435
+ .stuic-button[data-icon-edge="leading"][data-size="md"] {
436
+ padding-inline-start: var(--stuic-button-padding-y-md);
437
+ }
438
+ .stuic-button[data-icon-edge="leading"][data-size="lg"] {
439
+ padding-inline-start: var(--stuic-button-padding-y-lg);
440
+ }
441
+ .stuic-button[data-icon-edge="leading"][data-size="xl"] {
442
+ padding-inline-start: var(--stuic-button-padding-y-xl);
443
+ }
444
+ .stuic-button[data-icon-edge="trailing"][data-size="sm"] {
445
+ padding-inline-end: var(--stuic-button-padding-y-sm);
446
+ }
447
+ .stuic-button[data-icon-edge="trailing"][data-size="md"] {
448
+ padding-inline-end: var(--stuic-button-padding-y-md);
449
+ }
450
+ .stuic-button[data-icon-edge="trailing"][data-size="lg"] {
451
+ padding-inline-end: var(--stuic-button-padding-y-lg);
452
+ }
453
+ .stuic-button[data-icon-edge="trailing"][data-size="xl"] {
454
+ padding-inline-end: var(--stuic-button-padding-y-xl);
455
+ }
456
+
425
457
  /* ============================================================================
426
458
  ICON BUTTON
427
459
  Semantic marker for icon-only buttons. Layout is handled by [data-aspect1].
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.104.0",
3
+ "version": "3.106.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",