@kalink-ui/seedly 0.25.0 → 0.26.1
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/CHANGELOG.md +23 -1
- package/package.json +2 -2
- package/src/components/box/box.css.ts +21 -8
- package/src/components/box/box.responsive.ts +9 -0
- package/src/components/box/box.tsx +9 -3
- package/src/components/button/button.css.ts +199 -177
- package/src/components/button/button.responsive.ts +11 -0
- package/src/components/button/button.tsx +31 -10
- package/src/components/card/card.tsx +3 -1
- package/src/components/center/center.css.ts +23 -10
- package/src/components/center/center.responsive.ts +9 -0
- package/src/components/center/center.tsx +12 -3
- package/src/components/cluster/cluster.css.ts +116 -89
- package/src/components/cluster/cluster.responsive.ts +9 -0
- package/src/components/cluster/cluster.tsx +14 -3
- package/src/components/command/command-list.css.ts +24 -10
- package/src/components/command/command-list.responsive.ts +9 -0
- package/src/components/command/command-list.tsx +2 -2
- package/src/components/command/command.tsx +2 -2
- package/src/components/cover/cover.css.ts +23 -10
- package/src/components/cover/cover.responsive.ts +9 -0
- package/src/components/cover/cover.tsx +7 -3
- package/src/components/grid/grid.css.ts +21 -8
- package/src/components/grid/grid.responsive.ts +9 -0
- package/src/components/grid/grid.tsx +7 -3
- package/src/components/heading/heading.css.ts +75 -49
- package/src/components/heading/heading.responsive.ts +28 -0
- package/src/components/heading/heading.tsx +18 -9
- package/src/components/loader/loader.css.ts +31 -18
- package/src/components/loader/moon-loader.responsive.ts +9 -0
- package/src/components/loader/moon-loader.tsx +12 -3
- package/src/components/menu/menu-separator.css.ts +26 -10
- package/src/components/menu/menu-separator.responsive.ts +9 -0
- package/src/components/sidebar/sidebar.css.ts +21 -8
- package/src/components/sidebar/sidebar.responsive.ts +9 -0
- package/src/components/sidebar/sidebar.tsx +6 -3
- package/src/components/stack/stack.css.ts +61 -39
- package/src/components/stack/stack.responsive.ts +9 -0
- package/src/components/stack/stack.tsx +10 -3
- package/src/components/switcher/switcher.css.ts +21 -8
- package/src/components/switcher/switcher.responsive.ts +9 -0
- package/src/components/switcher/switcher.tsx +7 -3
- package/src/components/text/text.css.ts +78 -39
- package/src/components/text/text.responsive.ts +69 -0
- package/src/components/text/text.tsx +35 -8
- package/src/styles/breakpoints.ts +25 -0
- package/src/styles/index.ts +12 -0
- package/src/styles/responsive.ts +180 -0
|
@@ -2,10 +2,17 @@ import { PolymorphicComponentProps } from '@kalink-ui/dibbly';
|
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
3
|
import { ElementType } from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { StackVariants } from './stack.css';
|
|
6
|
+
import { stackResponsive } from './stack.responsive';
|
|
7
|
+
|
|
8
|
+
import type { Responsive } from '../../styles/responsive';
|
|
6
9
|
|
|
7
10
|
export type StackProps<TUse extends ElementType> =
|
|
8
|
-
PolymorphicComponentProps<TUse> &
|
|
11
|
+
PolymorphicComponentProps<TUse> &
|
|
12
|
+
Omit<StackVariants, 'spacing' | 'align'> & {
|
|
13
|
+
spacing?: Responsive<NonNullable<StackVariants['spacing']>>;
|
|
14
|
+
align?: Responsive<NonNullable<StackVariants['align']>>;
|
|
15
|
+
};
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* A custom element for injecting white space (margin) between flow
|
|
@@ -27,7 +34,7 @@ export function Stack<TUse extends ElementType = 'div'>({
|
|
|
27
34
|
|
|
28
35
|
return (
|
|
29
36
|
<Comp
|
|
30
|
-
className={clsx(
|
|
37
|
+
className={clsx(stackResponsive({ spacing, align }), className)}
|
|
31
38
|
{...rest}
|
|
32
39
|
/>
|
|
33
40
|
);
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { createVar, globalStyle } from '@vanilla-extract/css';
|
|
2
2
|
import { recipe, type RecipeVariants } from '@vanilla-extract/recipes';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createResponsiveVariants,
|
|
6
|
+
defaultMedia,
|
|
7
|
+
sys,
|
|
8
|
+
mapContractVars,
|
|
9
|
+
} from '../../styles';
|
|
5
10
|
import { components } from '../../styles/layers.css';
|
|
6
11
|
|
|
7
12
|
export const thresholdVar = createVar();
|
|
8
13
|
export const limitVar = createVar();
|
|
9
14
|
|
|
15
|
+
// Shared variant styles to support responsive overrides
|
|
16
|
+
export const switcherSpacingStyles = mapContractVars(sys.spacing, (key) => ({
|
|
17
|
+
'@layer': {
|
|
18
|
+
[components]: {
|
|
19
|
+
gap: sys.spacing[key],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
10
24
|
export const switcherRecipe = recipe({
|
|
11
25
|
base: {
|
|
12
26
|
'@layer': {
|
|
@@ -25,13 +39,7 @@ export const switcherRecipe = recipe({
|
|
|
25
39
|
/**
|
|
26
40
|
* The space (margin) between the child elements
|
|
27
41
|
*/
|
|
28
|
-
spacing:
|
|
29
|
-
'@layer': {
|
|
30
|
-
[components]: {
|
|
31
|
-
gap: sys.spacing[key],
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
})),
|
|
42
|
+
spacing: switcherSpacingStyles,
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
45
|
* The maximum number of elements allowed to appear in the horizontal configuration
|
|
@@ -123,3 +131,8 @@ globalStyle(
|
|
|
123
131
|
export type SwitcherVariants = NonNullable<
|
|
124
132
|
RecipeVariants<typeof switcherRecipe>
|
|
125
133
|
>;
|
|
134
|
+
|
|
135
|
+
export const spacingAt = createResponsiveVariants({
|
|
136
|
+
styles: switcherSpacingStyles,
|
|
137
|
+
media: defaultMedia,
|
|
138
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defaultOrder, responsiveRecipe } from '../../styles/responsive';
|
|
2
|
+
|
|
3
|
+
import { spacingAt, switcherRecipe } from './switcher.css';
|
|
4
|
+
|
|
5
|
+
export const switcherResponsive = responsiveRecipe({
|
|
6
|
+
recipe: switcherRecipe,
|
|
7
|
+
at: { spacing: spacingAt },
|
|
8
|
+
order: defaultOrder,
|
|
9
|
+
});
|
|
@@ -5,15 +5,19 @@ import { assignInlineVars } from '@vanilla-extract/dynamic';
|
|
|
5
5
|
import { clsx } from 'clsx';
|
|
6
6
|
import { ElementType } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { SwitcherVariants, thresholdVar } from './switcher.css';
|
|
9
|
+
import { switcherResponsive } from './switcher.responsive';
|
|
10
|
+
|
|
11
|
+
import type { Responsive } from '../../styles/responsive';
|
|
9
12
|
|
|
10
13
|
export type SwitcherProps<TUse extends ElementType> =
|
|
11
14
|
PolymorphicComponentProps<TUse> &
|
|
12
|
-
SwitcherVariants & {
|
|
15
|
+
Omit<SwitcherVariants, 'spacing'> & {
|
|
13
16
|
/**
|
|
14
17
|
* The threshold at which to switch between horizontal and vertical layouts
|
|
15
18
|
*/
|
|
16
19
|
threshold?: string;
|
|
20
|
+
spacing?: Responsive<NonNullable<SwitcherVariants['spacing']>>;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -33,7 +37,7 @@ export function Switcher<TUse extends ElementType>({
|
|
|
33
37
|
|
|
34
38
|
return (
|
|
35
39
|
<Comp
|
|
36
|
-
className={clsx(
|
|
40
|
+
className={clsx(switcherResponsive({ spacing, limit }), className)}
|
|
37
41
|
style={assignInlineVars({
|
|
38
42
|
...(threshold && { [thresholdVar]: threshold }),
|
|
39
43
|
})}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createVar, style } from '@vanilla-extract/css';
|
|
1
|
+
import { createVar, style, type StyleRule } from '@vanilla-extract/css';
|
|
2
2
|
import { recipe, type RecipeVariants } from '@vanilla-extract/recipes';
|
|
3
3
|
|
|
4
|
+
import { createResponsiveVariants, defaultMedia, sys } from '../../styles';
|
|
4
5
|
import { components } from '../../styles/layers.css';
|
|
5
6
|
|
|
6
7
|
export const lineClampNumber = createVar();
|
|
@@ -17,6 +18,46 @@ const lineClamp = style({
|
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
// Extract align styles for responsive overrides
|
|
22
|
+
export const textAlignStyles = {
|
|
23
|
+
start: {
|
|
24
|
+
'@layer': {
|
|
25
|
+
[components]: {
|
|
26
|
+
vars: {
|
|
27
|
+
[textAlign]: 'start',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
center: {
|
|
33
|
+
'@layer': {
|
|
34
|
+
[components]: {
|
|
35
|
+
vars: {
|
|
36
|
+
[textAlign]: 'center',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
end: {
|
|
42
|
+
'@layer': {
|
|
43
|
+
[components]: {
|
|
44
|
+
vars: {
|
|
45
|
+
[textAlign]: 'end',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
justify: {
|
|
51
|
+
'@layer': {
|
|
52
|
+
[components]: {
|
|
53
|
+
vars: {
|
|
54
|
+
[textAlign]: 'justify',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
20
61
|
export const textRecipe = recipe({
|
|
21
62
|
base: {
|
|
22
63
|
'@layer': {
|
|
@@ -139,44 +180,7 @@ export const textRecipe = recipe({
|
|
|
139
180
|
/**
|
|
140
181
|
* Controls the alignment of the text.
|
|
141
182
|
*/
|
|
142
|
-
align:
|
|
143
|
-
start: {
|
|
144
|
-
'@layer': {
|
|
145
|
-
[components]: {
|
|
146
|
-
vars: {
|
|
147
|
-
[textAlign]: 'start',
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
center: {
|
|
153
|
-
'@layer': {
|
|
154
|
-
[components]: {
|
|
155
|
-
vars: {
|
|
156
|
-
[textAlign]: 'center',
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
end: {
|
|
162
|
-
'@layer': {
|
|
163
|
-
[components]: {
|
|
164
|
-
vars: {
|
|
165
|
-
[textAlign]: 'end',
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
justify: {
|
|
171
|
-
'@layer': {
|
|
172
|
-
[components]: {
|
|
173
|
-
vars: {
|
|
174
|
-
[textAlign]: 'justify',
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
},
|
|
183
|
+
align: textAlignStyles,
|
|
180
184
|
},
|
|
181
185
|
});
|
|
182
186
|
|
|
@@ -189,3 +193,38 @@ export const textEllipsisWrapper = style({
|
|
|
189
193
|
});
|
|
190
194
|
|
|
191
195
|
export type TextVariants = NonNullable<RecipeVariants<typeof textRecipe>>;
|
|
196
|
+
|
|
197
|
+
export const alignAt = createResponsiveVariants({
|
|
198
|
+
styles: textAlignStyles,
|
|
199
|
+
media: defaultMedia,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Responsive typography overrides for variant+size combos
|
|
203
|
+
const typographyComboEntries = Object.entries(sys.typography).flatMap(
|
|
204
|
+
([variantKey, sizes]) => {
|
|
205
|
+
return Object.entries(sizes).map(([sizeKey, v]) => {
|
|
206
|
+
const k = `${variantKey}.${sizeKey}`;
|
|
207
|
+
const rule: StyleRule = {
|
|
208
|
+
'@layer': {
|
|
209
|
+
[components]: {
|
|
210
|
+
fontFamily: v.font,
|
|
211
|
+
fontWeight: v.weight,
|
|
212
|
+
lineHeight: v.lineHeight,
|
|
213
|
+
letterSpacing: v.tracking,
|
|
214
|
+
fontSize: v.size,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return [k, rule] as const;
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
export const typographyAt = createResponsiveVariants({
|
|
225
|
+
styles: Object.fromEntries(typographyComboEntries) as Record<
|
|
226
|
+
string,
|
|
227
|
+
StyleRule
|
|
228
|
+
>,
|
|
229
|
+
media: defaultMedia,
|
|
230
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultOrder,
|
|
3
|
+
resolveResponsive,
|
|
4
|
+
responsiveRecipe,
|
|
5
|
+
TypographySize,
|
|
6
|
+
TypographyVariant,
|
|
7
|
+
type BreakpointWithBase,
|
|
8
|
+
type Responsive,
|
|
9
|
+
} from '../../styles';
|
|
10
|
+
|
|
11
|
+
import { alignAt, textRecipe, typographyAt } from './text.css';
|
|
12
|
+
|
|
13
|
+
export const textResponsive = responsiveRecipe({
|
|
14
|
+
recipe: textRecipe,
|
|
15
|
+
at: { align: alignAt },
|
|
16
|
+
order: defaultOrder,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function buildTypographyOverrides(opts: {
|
|
20
|
+
variant?: Responsive<TypographyVariant>;
|
|
21
|
+
size?: Responsive<TypographySize>;
|
|
22
|
+
}) {
|
|
23
|
+
const { variant, size } = opts;
|
|
24
|
+
const varMap = resolveResponsive<TypographyVariant, BreakpointWithBase>(
|
|
25
|
+
variant,
|
|
26
|
+
defaultOrder,
|
|
27
|
+
);
|
|
28
|
+
const sizeMap = resolveResponsive<TypographySize, BreakpointWithBase>(
|
|
29
|
+
size,
|
|
30
|
+
defaultOrder,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Carry forward variant/size values across breakpoints so that
|
|
34
|
+
// a value set at md persists to lg/xl/... unless overridden again.
|
|
35
|
+
let currentVariant = varMap.xs ?? (variant as TypographyVariant | undefined);
|
|
36
|
+
let currentSize = sizeMap.xs ?? (size as TypographySize | undefined);
|
|
37
|
+
|
|
38
|
+
const classes: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const bp of defaultOrder) {
|
|
41
|
+
if (bp === 'xs') {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (varMap[bp] != null) {
|
|
46
|
+
currentVariant = varMap[bp];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sizeMap[bp] != null) {
|
|
50
|
+
currentSize = sizeMap[bp];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (currentVariant && currentSize) {
|
|
54
|
+
const key = `${String(currentVariant)}.${String(currentSize)}`;
|
|
55
|
+
const cls = (
|
|
56
|
+
typographyAt as Record<
|
|
57
|
+
Exclude<BreakpointWithBase, 'xs'>,
|
|
58
|
+
Record<string, string>
|
|
59
|
+
>
|
|
60
|
+
)[bp]?.[key];
|
|
61
|
+
|
|
62
|
+
if (cls) {
|
|
63
|
+
classes.push(cls);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return classes;
|
|
69
|
+
}
|
|
@@ -4,20 +4,42 @@ import { ElementType } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { TypographySize, TypographyVariant, typography } from '../../styles';
|
|
6
6
|
|
|
7
|
-
import { TextVariants,
|
|
7
|
+
import { TextVariants, textEllipsisWrapper } from './text.css';
|
|
8
|
+
import { buildTypographyOverrides, textResponsive } from './text.responsive';
|
|
9
|
+
|
|
10
|
+
import type { Responsive } from '../../styles/responsive';
|
|
11
|
+
|
|
12
|
+
function getBase<T extends string | number>(value: Responsive<T> | undefined) {
|
|
13
|
+
if (value == null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return value[0] ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === 'object') {
|
|
22
|
+
const obj = value as Partial<Record<string, T>> & { xs?: T };
|
|
23
|
+
|
|
24
|
+
return obj.xs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
8
29
|
|
|
9
30
|
export type TextProps<TUse extends React.ElementType> =
|
|
10
31
|
PolymorphicComponentProps<TUse> &
|
|
11
|
-
TextVariants & {
|
|
32
|
+
Omit<TextVariants, 'align'> & {
|
|
12
33
|
/**
|
|
13
34
|
* The size of the typography used to render the text.
|
|
14
35
|
*/
|
|
15
|
-
size?: TypographySize
|
|
36
|
+
size?: Responsive<TypographySize>;
|
|
16
37
|
|
|
17
38
|
/**
|
|
18
39
|
* The typography variant used to render the text.
|
|
19
40
|
*/
|
|
20
|
-
variant?: TypographyVariant
|
|
41
|
+
variant?: Responsive<TypographyVariant>;
|
|
42
|
+
align?: Responsive<NonNullable<TextVariants['align']>>;
|
|
21
43
|
};
|
|
22
44
|
|
|
23
45
|
export function Text<TUse extends ElementType>(props: TextProps<TUse>) {
|
|
@@ -26,19 +48,24 @@ export function Text<TUse extends ElementType>(props: TextProps<TUse>) {
|
|
|
26
48
|
className,
|
|
27
49
|
truncate,
|
|
28
50
|
lineClamp,
|
|
29
|
-
size
|
|
51
|
+
size,
|
|
30
52
|
use: Comp = 'span',
|
|
31
|
-
variant
|
|
53
|
+
variant,
|
|
32
54
|
wrap,
|
|
33
55
|
align,
|
|
34
56
|
...rest
|
|
35
57
|
} = props;
|
|
36
58
|
|
|
59
|
+
const typographyOverrides = buildTypographyOverrides({ variant, size });
|
|
60
|
+
const baseVariant = getBase(variant) ?? 'body';
|
|
61
|
+
const baseSize = getBase(size) ?? 'medium';
|
|
62
|
+
|
|
37
63
|
return (
|
|
38
64
|
<Comp
|
|
39
65
|
className={clsx(
|
|
40
|
-
getProp(typography, `${
|
|
41
|
-
|
|
66
|
+
getProp(typography, `${baseVariant}.${baseSize}`),
|
|
67
|
+
typographyOverrides,
|
|
68
|
+
textResponsive({ truncate, lineClamp, align, wrap }),
|
|
42
69
|
className,
|
|
43
70
|
)}
|
|
44
71
|
{...rest}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const breakpoints = {
|
|
2
|
+
sm: 568,
|
|
3
|
+
md: 768,
|
|
4
|
+
lg: 1024,
|
|
5
|
+
xl: 1280,
|
|
6
|
+
'2xl': 1600,
|
|
7
|
+
'3xl': 1920,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type BreakpointKey = keyof typeof breakpoints;
|
|
11
|
+
|
|
12
|
+
export const screen = {
|
|
13
|
+
maxXs: `(max-width: ${breakpoints.sm - 1}px)`,
|
|
14
|
+
sm: `(min-width: ${breakpoints.sm}px)`,
|
|
15
|
+
maxSm: `(max-width: ${breakpoints.md - 1}px)`,
|
|
16
|
+
md: `(min-width: ${breakpoints.md}px)`,
|
|
17
|
+
maxMd: `(max-width: ${breakpoints.lg - 1}px)`,
|
|
18
|
+
lg: `(min-width: ${breakpoints.lg}px)`,
|
|
19
|
+
maxLg: `(max-width: ${breakpoints.xl - 1}px)`,
|
|
20
|
+
xl: `(min-width: ${breakpoints.xl}px)`,
|
|
21
|
+
maxXl: `(max-width: ${breakpoints['2xl'] - 1}px)`,
|
|
22
|
+
'2xl': `(min-width: ${breakpoints['2xl']}px)`,
|
|
23
|
+
max2xl: `(max-width: ${breakpoints['3xl'] - 1}px)`,
|
|
24
|
+
'3xl': `(min-width: ${breakpoints['3xl']}px)`,
|
|
25
|
+
} as const;
|
package/src/styles/index.ts
CHANGED
|
@@ -22,3 +22,15 @@ export {
|
|
|
22
22
|
export { transition, type CreateTransitionFn } from './transition';
|
|
23
23
|
|
|
24
24
|
export { visuallyHidden } from './visually-hidden.css';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
responsiveRecipe,
|
|
28
|
+
createResponsiveVariants,
|
|
29
|
+
resolveResponsive,
|
|
30
|
+
defaultMedia,
|
|
31
|
+
defaultOrder,
|
|
32
|
+
type Responsive,
|
|
33
|
+
type BreakpointWithBase,
|
|
34
|
+
} from './responsive';
|
|
35
|
+
|
|
36
|
+
export { breakpoints, screen, type BreakpointKey } from './breakpoints';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { styleVariants, type StyleRule } from '@vanilla-extract/css';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
|
|
4
|
+
import { screen, type BreakpointKey } from './breakpoints';
|
|
5
|
+
|
|
6
|
+
export type BreakpointWithBase = 'xs' | BreakpointKey;
|
|
7
|
+
|
|
8
|
+
export type ResponsiveObject<T, B extends string> = Partial<
|
|
9
|
+
Record<Exclude<B, 'xs'>, T>
|
|
10
|
+
> & { xs?: T };
|
|
11
|
+
export type ResponsiveArray<T> = (T | null | undefined)[];
|
|
12
|
+
export type Responsive<T, B extends string = BreakpointWithBase> =
|
|
13
|
+
| T
|
|
14
|
+
| ResponsiveObject<T, B>
|
|
15
|
+
| ResponsiveArray<T>;
|
|
16
|
+
|
|
17
|
+
export function resolveResponsive<T, B extends string = BreakpointWithBase>(
|
|
18
|
+
value: Responsive<T, B> | undefined,
|
|
19
|
+
order: readonly B[],
|
|
20
|
+
): Partial<Record<B, T>> {
|
|
21
|
+
if (value == null) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
const out: Partial<Record<B, T>> = {};
|
|
27
|
+
|
|
28
|
+
value.forEach((val, i) => {
|
|
29
|
+
const breakpoint = order[i] as B | undefined;
|
|
30
|
+
|
|
31
|
+
if (val != null && breakpoint) {
|
|
32
|
+
out[breakpoint] = val as T;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === 'object') {
|
|
40
|
+
return value as Partial<Record<B, T>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { xs: value as T } as Partial<Record<B, T>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CreateResponsiveVariantsArgs<
|
|
47
|
+
VariantValues extends string | number,
|
|
48
|
+
Bps extends string,
|
|
49
|
+
> {
|
|
50
|
+
styles: Record<VariantValues, StyleRule | StyleRule[]>;
|
|
51
|
+
media: Partial<Record<Exclude<Bps, 'xs'>, string>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createResponsiveVariants<
|
|
55
|
+
const VariantValues extends string | number,
|
|
56
|
+
const Bps extends string,
|
|
57
|
+
>(args: CreateResponsiveVariantsArgs<VariantValues, Bps>) {
|
|
58
|
+
const { styles, media } = args;
|
|
59
|
+
|
|
60
|
+
const at = Object.fromEntries(
|
|
61
|
+
Object.entries(media).map(([bp, query]) => {
|
|
62
|
+
const styleEntries = Object.entries(styles) as [
|
|
63
|
+
VariantValues,
|
|
64
|
+
StyleRule | StyleRule[],
|
|
65
|
+
][];
|
|
66
|
+
|
|
67
|
+
const styleMap = Object.fromEntries(
|
|
68
|
+
styleEntries.map(([val, rule]) => [
|
|
69
|
+
val,
|
|
70
|
+
applyMedia(query as string, rule),
|
|
71
|
+
]),
|
|
72
|
+
) as Record<VariantValues, StyleRule | StyleRule[]>;
|
|
73
|
+
|
|
74
|
+
return [bp, styleVariants(styleMap)];
|
|
75
|
+
}),
|
|
76
|
+
) as Record<Exclude<Bps, 'xs'>, Record<VariantValues, string>>;
|
|
77
|
+
|
|
78
|
+
return at;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function applyMedia(
|
|
82
|
+
query: string,
|
|
83
|
+
rule: StyleRule | StyleRule[],
|
|
84
|
+
): StyleRule | StyleRule[] {
|
|
85
|
+
if (Array.isArray(rule)) {
|
|
86
|
+
return rule.map((r) => ({ '@media': { [query]: r } })) as StyleRule[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { '@media': { [query]: rule } } as StyleRule;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type MakeResponsive<V, B extends string> = {
|
|
93
|
+
[K in keyof V]: V[K] extends string | number ? Responsive<V[K], B> : V[K];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export interface ResponsiveRecipeArgs<V, Bps extends string> {
|
|
97
|
+
recipe: (props: V) => string;
|
|
98
|
+
at: Partial<
|
|
99
|
+
Record<keyof V, Record<Exclude<Bps, 'xs'>, Record<string, string>>>
|
|
100
|
+
>;
|
|
101
|
+
order: readonly Bps[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function responsiveRecipe<
|
|
105
|
+
V extends Record<string, unknown>,
|
|
106
|
+
const Bps extends string,
|
|
107
|
+
>(args: ResponsiveRecipeArgs<V, Bps>) {
|
|
108
|
+
const { recipe, at, order } = args;
|
|
109
|
+
|
|
110
|
+
return (props: MakeResponsive<V, Bps> & { className?: string }) => {
|
|
111
|
+
const { className, ...rest } = props as Record<string, unknown> & {
|
|
112
|
+
className?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const baseProps: Partial<V> = {};
|
|
116
|
+
|
|
117
|
+
for (const key in rest) {
|
|
118
|
+
const value = (rest as Record<string, unknown>)[key];
|
|
119
|
+
const map = resolveResponsive(value, order);
|
|
120
|
+
|
|
121
|
+
(baseProps as Record<string, unknown>)[key] =
|
|
122
|
+
(map as Record<string, unknown>)['xs'] ?? value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const base = recipe(baseProps as V);
|
|
126
|
+
const overrides: string[] = [];
|
|
127
|
+
|
|
128
|
+
for (const key in rest) {
|
|
129
|
+
const value = (rest as Record<string, unknown>)[key];
|
|
130
|
+
const map = resolveResponsive(value, order);
|
|
131
|
+
const variantAt = (at as Record<string, unknown>)[key] as
|
|
132
|
+
| Record<string, Record<string, string>>
|
|
133
|
+
| undefined;
|
|
134
|
+
|
|
135
|
+
if (!variantAt) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const bp of order) {
|
|
140
|
+
if (bp === 'xs') {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const val = (map as Partial<Record<Bps, unknown>>)[bp];
|
|
145
|
+
|
|
146
|
+
if (val == null) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cls = variantAt[bp as string]?.[String(val)];
|
|
151
|
+
|
|
152
|
+
if (cls) {
|
|
153
|
+
overrides.push(cls);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return clsx(base, ...overrides, className);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Default viewport media queries (can be replaced per-component if needed)
|
|
163
|
+
export const defaultMedia = {
|
|
164
|
+
sm: screen.sm,
|
|
165
|
+
md: screen.md,
|
|
166
|
+
lg: screen.lg,
|
|
167
|
+
xl: screen.xl,
|
|
168
|
+
'2xl': screen['2xl'],
|
|
169
|
+
'3xl': screen['3xl'],
|
|
170
|
+
} as const satisfies Partial<Record<BreakpointKey, string>>;
|
|
171
|
+
|
|
172
|
+
export const defaultOrder: readonly BreakpointWithBase[] = [
|
|
173
|
+
'xs',
|
|
174
|
+
'sm',
|
|
175
|
+
'md',
|
|
176
|
+
'lg',
|
|
177
|
+
'xl',
|
|
178
|
+
'2xl',
|
|
179
|
+
'3xl',
|
|
180
|
+
] as const;
|