@urbicon-ui/blocks 6.1.8 → 6.2.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/README.md CHANGED
@@ -45,10 +45,10 @@ Available effects: `scale`, `ripple`, `translate`, `glow`, `bounce`, `pulse`, `s
45
45
 
46
46
  ## Presets & Defaults
47
47
 
48
- `BlocksProvider` registers project-wide defaults and named presets. Override hierarchy:
48
+ `BlocksProvider` registers project-wide defaults, named presets, and prop-conditional `overrides`. Override hierarchy (conflict-resolved per Tailwind bucket — a later source wins):
49
49
 
50
50
  ```
51
- tv()-DefaultsBlocksProvider defaults → BlocksProvider presetsInstance slotClasses → Instance class
51
+ tv() defaults defaults.slotClasses defaults.overridespreset.slotClasses preset.overrides instance slotClasses → instance class
52
52
  ```
53
53
 
54
54
  ```svelte
@@ -61,6 +61,16 @@ tv()-Defaults → BlocksProvider defaults → BlocksProvider presets → Instanc
61
61
  </BlocksProvider>
62
62
  ```
63
63
 
64
+ Use `overrides` for **prop-conditional** rules an unconditional `slotClasses` cannot express (e.g. only the `outlined` variant). Each entry is a `compoundVariant`-shaped matcher; the tv() conflict resolver strips the library's conflicting class:
65
+
66
+ ```svelte
67
+ <BlocksProvider
68
+ defaults={{ Badge: { overrides: [{ variant: 'outlined', class: { base: 'border' } }] } }}
69
+ >
70
+ <!-- outlined badges get a 1px border; other variants untouched -->
71
+ </BlocksProvider>
72
+ ```
73
+
64
74
  ## Icons
65
75
 
66
76
  156 hand-rolled SVG icons in `src/lib/icons/`, registered via `IconProvider`. Metadata (`ICON_METADATA`) enables search by name, keyword, or category. See the icon-design rules in [AGENTS.md → Icon Design Rules](../../AGENTS.md#icon-design-rules).
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Button, mintRegistry } from '../..';
3
- import { getBlocksConfig, mergeSlotClasses, resolvePresetSlotClasses } from '../../provider';
3
+ import { getBlocksConfig, resolveSlotClasses } from '../../provider';
4
4
  import { resolveIcon } from '../../icons';
5
5
  import CloseIconDefault from '../../icons/CloseIcon.svelte';
6
6
  import { useBlocksI18n } from '../..';
@@ -42,13 +42,6 @@
42
42
 
43
43
  const blocksConfig = getBlocksConfig();
44
44
  const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
45
- const slotClasses = $derived(
46
- mergeSlotClasses(
47
- blocksConfig?.defaults?.Badge?.slotClasses,
48
- resolvePresetSlotClasses(blocksConfig?.presets, 'Badge', preset),
49
- slotClassesProp
50
- )
51
- );
52
45
 
53
46
  let badgeElement = $state<HTMLElement>();
54
47
  let isHovered = $state(false);
@@ -57,20 +50,24 @@
57
50
  const isInteractive = $derived(interactive || !!onclick);
58
51
  const isRemovable = $derived(removable && !isDot);
59
52
 
60
- const styles = $derived(
61
- badgeVariants({
62
- tier: effectiveTier,
63
- intent,
64
- variant,
65
- size,
66
- counter: counter || undefined,
67
- pulse: pulse || undefined,
68
- removable: isRemovable || undefined,
69
- interactive: isInteractive || undefined,
70
- disabled: disabled || undefined,
71
- placement,
72
- border: border || undefined
73
- })
53
+ const variantProps = $derived({
54
+ tier: effectiveTier,
55
+ intent,
56
+ variant,
57
+ size,
58
+ counter: counter || undefined,
59
+ pulse: pulse || undefined,
60
+ removable: isRemovable || undefined,
61
+ interactive: isInteractive || undefined,
62
+ disabled: disabled || undefined,
63
+ placement,
64
+ border: border || undefined
65
+ });
66
+
67
+ const styles = $derived(badgeVariants(variantProps));
68
+
69
+ const slotClasses = $derived(
70
+ resolveSlotClasses(blocksConfig, 'Badge', preset, variantProps, slotClassesProp)
74
71
  );
75
72
 
76
73
  $effect(() => {
@@ -1,5 +1,28 @@
1
+ /**
2
+ * A prop-conditional style rule. Its non-`class` keys are matched against a
3
+ * component's active variant props — exactly like a `tv()` compoundVariant
4
+ * (`string` = equality, `string[]` = "one of"). On a match, the `class`
5
+ * record (slot → classes) is merged into the slot-class cascade. Additive:
6
+ * every matching rule contributes; later sources win per Tailwind bucket.
7
+ *
8
+ * @example
9
+ * { variant: 'outlined', class: { base: 'border' } } // 1px border only on outlined
10
+ */
11
+ export interface ConditionalOverride {
12
+ /** Per-slot classes applied when the prop conditions match. */
13
+ class: Record<string, string>;
14
+ /** Prop conditions: prop name → required value (or one of several). */
15
+ [propCondition: string]: string | string[] | Record<string, string> | undefined;
16
+ }
1
17
  export interface ComponentDefaults {
2
18
  slotClasses?: Record<string, string>;
19
+ /**
20
+ * Prop-conditional style rules, applied after unconditional `slotClasses`
21
+ * (so they win per bucket) but before instance-level `slotClasses` / `class`.
22
+ * Use for surgical per-variant tweaks the unconditional `slotClasses` cannot
23
+ * express, e.g. `overrides: [{ variant: 'outlined', class: { base: 'border' } }]`.
24
+ */
25
+ overrides?: ConditionalOverride[];
3
26
  }
4
27
  /**
5
28
  * A preset is a named, project-defined visual style for a component.
@@ -12,6 +35,8 @@ export interface ComponentDefaults {
12
35
  */
13
36
  export interface ComponentPreset {
14
37
  slotClasses?: Record<string, string>;
38
+ /** Prop-conditional rules scoped to this preset (see {@link ConditionalOverride}). */
39
+ overrides?: ConditionalOverride[];
15
40
  }
16
41
  /** Map of component name → preset name → preset definition. */
17
42
  export type PresetMap = Record<string, Record<string, ComponentPreset>>;
@@ -30,3 +55,23 @@ export declare function mergeSlotClasses(...sources: (Record<string, string> | u
30
55
  * is used but not registered, so missing presets are discoverable.
31
56
  */
32
57
  export declare function resolvePresetSlotClasses(presets: PresetMap | undefined, component: string, presetName: string | undefined): Record<string, string> | undefined;
58
+ /**
59
+ * Collect the per-slot classes of every `overrides` entry whose prop
60
+ * conditions match `activeProps`. Returns `undefined` when there are no
61
+ * overrides or none match. Multiple matches merge additively, in order.
62
+ */
63
+ export declare function resolveOverrideSlotClasses(overrides: ConditionalOverride[] | undefined, activeProps: Record<string, unknown>): Record<string, string> | undefined;
64
+ /**
65
+ * Resolve the full slot-class cascade for a component instance, honoring
66
+ * conditional `overrides` from both provider defaults and the active preset.
67
+ *
68
+ * Precedence (weak → strong), conflict-resolved per slot so a later source
69
+ * wins within the same Tailwind bucket:
70
+ *
71
+ * defaults.slotClasses → defaults.overrides[match]
72
+ * → preset.slotClasses → preset.overrides[match] → instance.slotClasses
73
+ *
74
+ * The result is handed to the component's `tv()` slot fn as the `class`
75
+ * override, where it additionally strips conflicting library classes.
76
+ */
77
+ export declare function resolveSlotClasses(config: BlocksConfig | undefined, component: string, preset: string | undefined, activeProps: Record<string, unknown>, instanceSlotClasses: Record<string, string> | undefined): Record<string, string>;
@@ -1,4 +1,5 @@
1
1
  import { createOptionalContext } from '../utils/optional-context';
2
+ import { matchesCompound, resolveClassChain } from '../utils/variants';
2
3
  // BlocksProvider is optional — components must work without one.
3
4
  const [getBlocksConfig, setBlocksConfig] = createOptionalContext();
4
5
  export { getBlocksConfig, setBlocksConfig };
@@ -34,3 +35,59 @@ export function resolvePresetSlotClasses(presets, component, presetName) {
34
35
  }
35
36
  return preset.slotClasses;
36
37
  }
38
+ /**
39
+ * Collect the per-slot classes of every `overrides` entry whose prop
40
+ * conditions match `activeProps`. Returns `undefined` when there are no
41
+ * overrides or none match. Multiple matches merge additively, in order.
42
+ */
43
+ export function resolveOverrideSlotClasses(overrides, activeProps) {
44
+ if (!overrides || overrides.length === 0)
45
+ return undefined;
46
+ let result;
47
+ for (const entry of overrides) {
48
+ if (!matchesCompound(entry, activeProps))
49
+ continue;
50
+ result ??= {};
51
+ for (const [slot, value] of Object.entries(entry.class)) {
52
+ if (!value)
53
+ continue;
54
+ result[slot] = result[slot] ? `${result[slot]} ${value}` : value;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+ /**
60
+ * Resolve the full slot-class cascade for a component instance, honoring
61
+ * conditional `overrides` from both provider defaults and the active preset.
62
+ *
63
+ * Precedence (weak → strong), conflict-resolved per slot so a later source
64
+ * wins within the same Tailwind bucket:
65
+ *
66
+ * defaults.slotClasses → defaults.overrides[match]
67
+ * → preset.slotClasses → preset.overrides[match] → instance.slotClasses
68
+ *
69
+ * The result is handed to the component's `tv()` slot fn as the `class`
70
+ * override, where it additionally strips conflicting library classes.
71
+ */
72
+ export function resolveSlotClasses(config, component, preset, activeProps, instanceSlotClasses) {
73
+ const defaults = config?.defaults?.[component];
74
+ const presetDef = preset ? config?.presets?.[component]?.[preset] : undefined;
75
+ const sources = [
76
+ defaults?.slotClasses,
77
+ resolveOverrideSlotClasses(defaults?.overrides, activeProps),
78
+ resolvePresetSlotClasses(config?.presets, component, preset),
79
+ resolveOverrideSlotClasses(presetDef?.overrides, activeProps),
80
+ instanceSlotClasses
81
+ ];
82
+ const result = {};
83
+ for (const source of sources) {
84
+ if (!source)
85
+ continue;
86
+ for (const [slot, value] of Object.entries(source)) {
87
+ if (!value)
88
+ continue;
89
+ result[slot] = result[slot] ? resolveClassChain(result[slot], value) : value;
90
+ }
91
+ }
92
+ return result;
93
+ }
@@ -1,2 +1,2 @@
1
1
  export { default as BlocksProvider } from './BlocksProvider.svelte';
2
- export { type BlocksConfig, type ComponentDefaults, type ComponentPreset, getBlocksConfig, mergeSlotClasses, type PresetMap, resolvePresetSlotClasses } from './blocks-context';
2
+ export { type BlocksConfig, type ComponentDefaults, type ComponentPreset, type ConditionalOverride, getBlocksConfig, mergeSlotClasses, type PresetMap, resolveOverrideSlotClasses, resolvePresetSlotClasses, resolveSlotClasses } from './blocks-context';
@@ -1,2 +1,2 @@
1
1
  export { default as BlocksProvider } from './BlocksProvider.svelte';
2
- export { getBlocksConfig, mergeSlotClasses, resolvePresetSlotClasses } from './blocks-context';
2
+ export { getBlocksConfig, mergeSlotClasses, resolveOverrideSlotClasses, resolvePresetSlotClasses, resolveSlotClasses } from './blocks-context';
@@ -27,6 +27,21 @@ export type VariantProps<T extends (...args: never[]) => unknown> = Omit<Exclude
27
27
  * pass. See `docs/ARCHITECTURE.md` "tv() engine — explicit trade-offs".
28
28
  */
29
29
  export declare function cx(...inputs: ClassInput[]): string;
30
+ /**
31
+ * Merge class strings with the same Tailwind conflict resolution `tv()`
32
+ * applies between stages: a later source's classes strip any earlier class
33
+ * that shares their conflict bucket, so the last source wins per bucket.
34
+ * Within a single source, order is preserved and conflicts fall through to
35
+ * the CSS cascade. Falsy / empty sources are skipped.
36
+ *
37
+ * Reuses `tokenize` + `stripConflicts`. Powers the BlocksProvider slot-class
38
+ * cascade (`resolveSlotClasses`) so a conditional `overrides` entry
39
+ * deterministically defeats an unconditional `slotClasses` entry in the same
40
+ * bucket — instead of emitting both and leaving the winner to stylesheet
41
+ * order.
42
+ */
43
+ export declare function resolveClassChain(...sources: (string | null | undefined)[]): string;
44
+ export declare function matchesCompound(compound: Record<string, unknown>, effectiveProps: Record<string, unknown>): boolean;
30
45
  export declare function tv<V extends Record<string, Record<string, unknown>> = {}>(config: {
31
46
  base?: string | string[];
32
47
  variants?: V;
@@ -317,6 +317,31 @@ function tokenize(value) {
317
317
  return [];
318
318
  return value.split(/\s+/).filter(Boolean);
319
319
  }
320
+ /**
321
+ * Merge class strings with the same Tailwind conflict resolution `tv()`
322
+ * applies between stages: a later source's classes strip any earlier class
323
+ * that shares their conflict bucket, so the last source wins per bucket.
324
+ * Within a single source, order is preserved and conflicts fall through to
325
+ * the CSS cascade. Falsy / empty sources are skipped.
326
+ *
327
+ * Reuses `tokenize` + `stripConflicts`. Powers the BlocksProvider slot-class
328
+ * cascade (`resolveSlotClasses`) so a conditional `overrides` entry
329
+ * deterministically defeats an unconditional `slotClasses` entry in the same
330
+ * bucket — instead of emitting both and leaving the winner to stylesheet
331
+ * order.
332
+ */
333
+ export function resolveClassChain(...sources) {
334
+ let acc = [];
335
+ for (const source of sources) {
336
+ if (!source)
337
+ continue;
338
+ const tokens = tokenize(source);
339
+ if (tokens.length === 0)
340
+ continue;
341
+ acc = [...stripConflicts(acc, tokens), ...tokens];
342
+ }
343
+ return acc.join(' ');
344
+ }
320
345
  // ─── Internal Helpers ────────────────────────────────────────────────────────
321
346
  function falsyToString(value) {
322
347
  if (value === false)
@@ -337,7 +362,7 @@ function stripUndefined(obj) {
337
362
  }
338
363
  return result;
339
364
  }
340
- function matchesCompound(compound, effectiveProps) {
365
+ export function matchesCompound(compound, effectiveProps) {
341
366
  for (const key of Object.keys(compound)) {
342
367
  if (key === 'class' || key === 'className')
343
368
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.1.8",
3
+ "version": "6.2.0",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,8 +90,8 @@
90
90
  "@sveltejs/package": "^2.5.8",
91
91
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
92
92
  "@tailwindcss/vite": "^4.3.1",
93
- "@urbicon-ui/i18n": "6.1.8",
94
- "@urbicon-ui/shared-types": "6.1.8",
93
+ "@urbicon-ui/i18n": "6.2.0",
94
+ "@urbicon-ui/shared-types": "6.2.0",
95
95
  "prettier": "^3.8.4",
96
96
  "prettier-plugin-svelte": "^4.1.1",
97
97
  "prettier-plugin-tailwindcss": "^0.8.0",