Skip to content

Commit 972a104

Browse files
authored
feat(icons): add IconMore export (#113)
* fix: design token * feat: update card tab * fix: dashboard * fix: mail page * fix: style * fix: pricing style * chore: add changeset for icon more export
1 parent 0305eaa commit 972a104

35 files changed

Lines changed: 4669 additions & 1612 deletions

.changeset/add-icon-more-export.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tiny-design/icons": minor
3+
---
4+
5+
Add the new `IconMore` export to the icons package.

apps/docs/src/containers/theme-studio/color-utils.ts

Lines changed: 180 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ type OklchColor = {
77
c: number;
88
h: number;
99
};
10+
export type ShadowValue = {
11+
color: string;
12+
opacity: number;
13+
blur: number;
14+
spread: number;
15+
offsetX: number;
16+
offsetY: number;
17+
};
1018

1119
function normalizeAlpha(value: number): number {
1220
if (!Number.isFinite(value)) return 1;
@@ -72,42 +80,66 @@ export function formatOklchColor(color: OklchColor): string {
7280
return `oklch(${color.l.toFixed(3)} ${color.c.toFixed(3)} ${color.h.toFixed(3)})`;
7381
}
7482

75-
export function deriveStatusPalette(styles: RuntimeStyles): Pick<
83+
export function deriveStatusPalette(
84+
styles: RuntimeStyles
85+
): Pick<
7686
ThemeEditorFields,
77-
'success' | 'successForeground' | 'info' | 'infoForeground' | 'warning' | 'warningForeground' | 'danger' | 'dangerForeground'
87+
| 'success'
88+
| 'successForeground'
89+
| 'info'
90+
| 'infoForeground'
91+
| 'warning'
92+
| 'warningForeground'
93+
| 'danger'
94+
| 'dangerForeground'
7895
> {
79-
const mode: ThemeMode = parseCssScalar(styles.background ?? '') != null
80-
? ((parseOklchColor(styles.background)?.l ?? 1) < 0.45 ? 'dark' : 'light')
81-
: 'light';
82-
const seed = parseOklchColor(styles.primary) ?? parseOklchColor(styles.accent) ?? parseOklchColor(styles.ring);
96+
const mode: ThemeMode =
97+
parseCssScalar(styles.background ?? '') != null
98+
? (parseOklchColor(styles.background)?.l ?? 1) < 0.45
99+
? 'dark'
100+
: 'light'
101+
: 'light';
102+
const seed =
103+
parseOklchColor(styles.primary) ??
104+
parseOklchColor(styles.accent) ??
105+
parseOklchColor(styles.ring);
83106
const chromaBase = clamp(seed?.c ?? (mode === 'dark' ? 0.17 : 0.19), 0.1, 0.24);
84107
const lightnessShift = seed ? (seed.l - (mode === 'dark' ? 0.78 : 0.58)) * 0.08 : 0;
85108
const hueShift = seed ? ((seed.h - 220) / 220) * 6 : 0;
86109
const statusForeground = mode === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(0.985 0 0)';
87110

88-
const createStatus = (hue: number, lightness: number, chromaScale: number) => formatOklchColor({
89-
l: clamp(lightness + lightnessShift, mode === 'dark' ? 0.68 : 0.54, mode === 'dark' ? 0.84 : 0.74),
90-
c: clamp(chromaBase * chromaScale, 0.12, 0.26),
91-
h: normalizeHue(hue + hueShift),
92-
});
111+
const createStatus = (hue: number, lightness: number, chromaScale: number) =>
112+
formatOklchColor({
113+
l: clamp(
114+
lightness + lightnessShift,
115+
mode === 'dark' ? 0.68 : 0.54,
116+
mode === 'dark' ? 0.84 : 0.74
117+
),
118+
c: clamp(chromaBase * chromaScale, 0.12, 0.26),
119+
h: normalizeHue(hue + hueShift),
120+
});
93121

94122
return {
95123
success: createStatus(148, mode === 'dark' ? 0.76 : 0.62, 0.92),
96124
successForeground: statusForeground,
97-
info: createStatus(238, mode === 'dark' ? 0.74 : 0.60, 0.96),
125+
info: createStatus(238, mode === 'dark' ? 0.74 : 0.6, 0.96),
98126
infoForeground: statusForeground,
99-
warning: createStatus(72, mode === 'dark' ? 0.80 : 0.69, 0.94),
127+
warning: createStatus(72, mode === 'dark' ? 0.8 : 0.69, 0.94),
100128
warningForeground: mode === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(0.205 0 0)',
101-
danger: createStatus(28, mode === 'dark' ? 0.72 : 0.60, 1),
129+
danger: createStatus(28, mode === 'dark' ? 0.72 : 0.6, 1),
102130
dangerForeground: statusForeground,
103131
};
104132
}
105133

106134
function parseHexColor(color: string): [number, number, number] | null {
107135
const normalized = color.trim().replace('#', '');
108-
const value = normalized.length === 3
109-
? normalized.split('').map((char) => `${char}${char}`).join('')
110-
: normalized;
136+
const value =
137+
normalized.length === 3
138+
? normalized
139+
.split('')
140+
.map((char) => `${char}${char}`)
141+
.join('')
142+
: normalized;
111143

112144
if (!/^[0-9a-f]{6}$/i.test(value)) return null;
113145

@@ -155,9 +187,138 @@ export function softenSurface(color: string, mode: ThemeMode, amount: number): s
155187
return mode === 'dark' ? tintColor(color, amount) : shadeColor(color, amount);
156188
}
157189

158-
export function buildShadow(styles: RuntimeStyles): string {
190+
function splitTopLevelTokens(value: string): string[] {
191+
const tokens: string[] = [];
192+
let current = '';
193+
let depth = 0;
194+
195+
for (const char of value) {
196+
if (char === '(') depth += 1;
197+
if (char === ')') depth = Math.max(0, depth - 1);
198+
199+
if (/\s/.test(char) && depth === 0) {
200+
if (current) {
201+
tokens.push(current);
202+
current = '';
203+
}
204+
continue;
205+
}
206+
207+
current += char;
208+
}
209+
210+
if (current) tokens.push(current);
211+
return tokens;
212+
}
213+
214+
function parsePxValue(token: string): number | null {
215+
const trimmed = token.trim();
216+
if (/^-?0(?:\.0+)?(?:px)?$/i.test(trimmed)) return 0;
217+
218+
const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(trimmed);
219+
if (!match) return null;
220+
221+
const parsed = Number.parseFloat(match[1]);
222+
return Number.isFinite(parsed) ? parsed : null;
223+
}
224+
225+
function rgbChannelToHex(value: string): string | null {
226+
const parsed = Number.parseInt(value.trim(), 10);
227+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 255) return null;
228+
return parsed.toString(16).padStart(2, '0');
229+
}
230+
231+
function parseShadowColor(value: string): Pick<ShadowValue, 'color' | 'opacity'> | null {
232+
const colorMixMatch =
233+
/^color-mix\(in srgb,\s*(.+?)\s+(-?\d+(?:\.\d+)?)%,\s*transparent\s*\)$/i.exec(value.trim());
234+
if (colorMixMatch) {
235+
const percentage = Number.parseFloat(colorMixMatch[2]);
236+
if (Number.isFinite(percentage)) {
237+
return {
238+
color: colorMixMatch[1].trim(),
239+
opacity: clamp(percentage / 100, 0, 1),
240+
};
241+
}
242+
}
243+
244+
const rgbaMatch = /^rgba\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^)]+)\)$/i.exec(
245+
value.trim()
246+
);
247+
if (rgbaMatch) {
248+
const red = rgbChannelToHex(rgbaMatch[1]);
249+
const green = rgbChannelToHex(rgbaMatch[2]);
250+
const blue = rgbChannelToHex(rgbaMatch[3]);
251+
const alpha = Number.parseFloat(rgbaMatch[4]);
252+
253+
if (red && green && blue && Number.isFinite(alpha)) {
254+
return {
255+
color: `#${red}${green}${blue}`,
256+
opacity: clamp(alpha, 0, 1),
257+
};
258+
}
259+
}
260+
261+
const rgbMatch = /^rgb\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*\)$/i.exec(value.trim());
262+
if (rgbMatch) {
263+
const red = rgbChannelToHex(rgbMatch[1]);
264+
const green = rgbChannelToHex(rgbMatch[2]);
265+
const blue = rgbChannelToHex(rgbMatch[3]);
266+
267+
if (red && green && blue) {
268+
return {
269+
color: `#${red}${green}${blue}`,
270+
opacity: 1,
271+
};
272+
}
273+
}
274+
275+
return value.trim()
276+
? {
277+
color: value.trim(),
278+
opacity: 1,
279+
}
280+
: null;
281+
}
282+
283+
export function formatShadowValue(shadow: ShadowValue): string {
284+
const offsetX = `${shadow.offsetX}px`;
285+
const offsetY = `${shadow.offsetY}px`;
286+
const blur = `${shadow.blur}px`;
287+
const spread = `${shadow.spread}px`;
288+
289+
return `${offsetX} ${offsetY} ${blur} ${spread} ${toRgba(shadow.color, shadow.opacity)}`;
290+
}
291+
292+
export function parseShadowValue(value: string, fallback: ShadowValue): ShadowValue {
293+
const trimmed = value.trim();
294+
if (!trimmed || trimmed.toLowerCase() === 'none') return fallback;
295+
296+
const tokens = splitTopLevelTokens(trimmed);
297+
if (tokens.length < 5) return fallback;
298+
299+
const offsetX = parsePxValue(tokens[0]);
300+
const offsetY = parsePxValue(tokens[1]);
301+
const blur = parsePxValue(tokens[2]);
302+
const spread = parsePxValue(tokens[3]);
303+
const color = parseShadowColor(tokens.slice(4).join(' '));
304+
305+
if (offsetX == null || offsetY == null || blur == null || spread == null || !color) {
306+
return fallback;
307+
}
308+
309+
return {
310+
color: color.color,
311+
opacity: color.opacity,
312+
blur,
313+
spread,
314+
offsetX,
315+
offsetY,
316+
};
317+
}
318+
319+
export function buildShadow(styles: RuntimeStyles, fallback = DEFAULT_FIELDS.shadowCard): string {
159320
const color = styles['shadow-color'];
160-
if (!color) return DEFAULT_FIELDS.shadowCard;
321+
if (!color) return fallback;
161322

162323
const opacity = Number.parseFloat(styles['shadow-opacity'] ?? '0.1');
163324
const blur = styles['shadow-blur'] ?? '0px';

apps/docs/src/containers/theme-studio/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const DEFAULT_FIELDS: ThemeEditorFields = {
5050
h2Size: '32px',
5151
letterSpacing: '-0.02em',
5252
radius: '0.3rem',
53+
shadowControl: 'none',
5354
shadowCard: '0 20px 55px rgba(17, 24, 39, 0.08)',
5455
shadowFocus: '0 0 0 3px rgba(110, 65, 191, 0.22)',
5556
buttonRadius: '0.3rem',

apps/docs/src/containers/theme-studio/editor-draft.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function buildPreviewVars(fields: ThemeEditorFields): React.CSSProperties
128128
'--editor-h2-size': fields.h2Size,
129129
'--editor-letter-spacing': fields.letterSpacing,
130130
'--editor-radius': fields.radius,
131+
'--editor-shadow-control': fields.shadowControl,
131132
'--editor-shadow-card': fields.shadowCard,
132133
'--editor-shadow-focus': fields.shadowFocus,
133134
'--editor-button-radius': fields.buttonRadius,

0 commit comments

Comments
 (0)