@urbicon-ui/blocks 6.1.7 → 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 +12 -2
- package/dist/primitives/Badge/Badge.svelte +19 -22
- package/dist/provider/blocks-context.d.ts +45 -0
- package/dist/provider/blocks-context.js +57 -0
- package/dist/provider/index.d.ts +1 -1
- package/dist/provider/index.js +1 -1
- package/dist/utils/variants.d.ts +15 -0
- package/dist/utils/variants.js +26 -1
- package/package.json +3 -3
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
|
|
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()
|
|
51
|
+
tv() defaults → defaults.slotClasses → defaults.overrides → preset.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,
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
}
|
package/dist/provider/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/provider/index.js
CHANGED
|
@@ -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';
|
package/dist/utils/variants.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/variants.js
CHANGED
|
@@ -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.
|
|
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.
|
|
94
|
-
"@urbicon-ui/shared-types": "6.
|
|
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",
|