glassdate-rn 0.1.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.
@@ -0,0 +1,126 @@
1
+ import type { GlassDateTokens } from '../types/tokens.types';
2
+
3
+ export const MONO_DEFAULT_ACCENT = '#4FC3F7';
4
+
5
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
6
+ const h = hex.replace('#', '');
7
+ return {
8
+ r: parseInt(h.slice(0, 2), 16),
9
+ g: parseInt(h.slice(2, 4), 16),
10
+ b: parseInt(h.slice(4, 6), 16),
11
+ };
12
+ }
13
+
14
+ function rgbToHex8(r: number, g: number, b: number, a: number): string {
15
+ const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
16
+ return (
17
+ '#' +
18
+ [r, g, b, a]
19
+ .map((v) => clamp(v).toString(16).padStart(2, '0'))
20
+ .join('')
21
+ );
22
+ }
23
+
24
+ function withAlpha(hex: string, opacity: number): string {
25
+ const { r, g, b } = hexToRgb(hex);
26
+ return rgbToHex8(r, g, b, Math.round(opacity * 255));
27
+ }
28
+
29
+ function relativeLuminance(hex: string): number {
30
+ const { r, g, b } = hexToRgb(hex);
31
+ const linearise = (c: number) => {
32
+ const s = c / 255;
33
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
34
+ };
35
+ return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b);
36
+ }
37
+
38
+ export function computeMonoTokens(accent: string): GlassDateTokens {
39
+ const lum = relativeLuminance(accent);
40
+ const useDarkText = lum > 0.179;
41
+ const textOnSurface = useDarkText ? '#1A1A2E' : '#F5F5FF';
42
+ const textOnAccent = useDarkText ? '#1A1A2E' : '#FFFFFF';
43
+ const popupBg = useDarkText ? '#FAFAFA' : '#1A1A28';
44
+ const popupText = useDarkText ? '#1A1A2E' : '#F5F5FF';
45
+
46
+ return {
47
+ scene: { backgroundColour: withAlpha(accent, 0.13) },
48
+ container: {
49
+ backgroundColour: withAlpha(accent, 0.06),
50
+ borderColour: withAlpha(accent, 0.22),
51
+ borderWidth: 1,
52
+ cornerRadius: 18,
53
+ shadowColour: accent,
54
+ shadowOffsetX: 0,
55
+ shadowOffsetY: 6,
56
+ shadowBlurRadius: 24,
57
+ shadowOpacity: 0.15,
58
+ blurRadius: 0,
59
+ },
60
+ pill: {
61
+ backgroundColour: withAlpha(accent, 0.1),
62
+ borderColour: withAlpha(accent, 0.25),
63
+ borderWidth: 1,
64
+ cornerRadius: 10,
65
+ textColour: accent,
66
+ fontWeight: 500,
67
+ hoverBackgroundColour: withAlpha(accent, 0.18),
68
+ todayBackgroundColour: accent,
69
+ todayBorderColour: accent,
70
+ todayTextColour: textOnAccent,
71
+ },
72
+ popup: {
73
+ backgroundColour: popupBg,
74
+ borderColour: withAlpha(accent, 0.2),
75
+ borderWidth: 1,
76
+ cornerRadius: 14,
77
+ itemTextColour: popupText,
78
+ itemFontWeight: 500,
79
+ itemHoverBackgroundColour: withAlpha(accent, 0.08),
80
+ itemActiveBackgroundColour: accent,
81
+ itemActiveTextColour: textOnAccent,
82
+ itemSelectedBackgroundColour: withAlpha(accent, 0.18),
83
+ itemSelectedTextColour: accent,
84
+ itemDisabledOpacity: 0.22,
85
+ },
86
+ nav: {
87
+ backgroundColour: withAlpha(accent, 0.1),
88
+ borderColour: withAlpha(accent, 0.22),
89
+ borderWidth: 1,
90
+ cornerRadius: 8,
91
+ arrowColour: accent,
92
+ hoverBackgroundColour: withAlpha(accent, 0.18),
93
+ disabledOpacity: 0.22,
94
+ },
95
+ grid: {
96
+ weekdayLabelColour: withAlpha(accent, 0.45),
97
+ weekdayFontWeight: 600,
98
+ dayTextColour: textOnSurface,
99
+ dayFontWeight: 400,
100
+ dayHoverBackgroundColour: withAlpha(accent, 0.08),
101
+ daySelectedBackgroundColour: accent,
102
+ daySelectedTextColour: textOnAccent,
103
+ dayTodayFontWeight: 700,
104
+ dayTodayDotColour: accent,
105
+ dayOutOfRangeOpacity: 0.18,
106
+ dayAdjacentMonthOpacity: 0.18,
107
+ },
108
+ footer: {
109
+ cancelBackgroundColour: withAlpha(accent, 0.07),
110
+ cancelBorderColour: withAlpha(accent, 0.18),
111
+ cancelBorderWidth: 1,
112
+ cancelTextColour: withAlpha(accent, 0.6),
113
+ cancelCornerRadius: 12,
114
+ cancelHoverBackgroundColour: withAlpha(accent, 0.14),
115
+ confirmBackgroundColour: accent,
116
+ confirmBorderColour: withAlpha(accent, 0.5),
117
+ confirmBorderWidth: 1,
118
+ confirmTextColour: textOnAccent,
119
+ confirmCornerRadius: 12,
120
+ confirmHoverBackgroundColour: withAlpha(accent, 0.87),
121
+ disabledOpacity: 0.38,
122
+ selectedDateTextColour: textOnSurface,
123
+ },
124
+ meta: { name: 'mono', mode: 'single', defaultAccent: accent },
125
+ };
126
+ }
@@ -0,0 +1,141 @@
1
+ import type { GlassDateTokens, ThemeKey, DeepPartial, DayShape } from '../types/tokens.types';
2
+ import { validateHexColour } from '../utils/colourUtils';
3
+ import { computeMonoTokens, MONO_DEFAULT_ACCENT } from './mono';
4
+
5
+ import defaultDark from '../../../tokens/themes/default.dark.json';
6
+ import defaultLight from '../../../tokens/themes/default.light.json';
7
+ import iosLiquidDark from '../../../tokens/themes/ios-liquid.dark.json';
8
+ import iosLiquidLight from '../../../tokens/themes/ios-liquid.light.json';
9
+ import material3Dark from '../../../tokens/themes/material3.dark.json';
10
+ import material3Light from '../../../tokens/themes/material3.light.json';
11
+ import toon from '../../../tokens/themes/toon.json';
12
+ import flat from '../../../tokens/themes/flat.json';
13
+ import plain from '../../../tokens/themes/plain.json';
14
+
15
+ type ColorScheme = 'dark' | 'light';
16
+
17
+ const THEME_MAP: Record<string, GlassDateTokens> = {
18
+ 'default.dark': defaultDark as unknown as GlassDateTokens,
19
+ 'default.light': defaultLight as unknown as GlassDateTokens,
20
+ 'ios-liquid.dark': iosLiquidDark as unknown as GlassDateTokens,
21
+ 'ios-liquid.light': iosLiquidLight as unknown as GlassDateTokens,
22
+ 'material3.dark': material3Dark as unknown as GlassDateTokens,
23
+ 'material3.light': material3Light as unknown as GlassDateTokens,
24
+ 'toon': toon as unknown as GlassDateTokens,
25
+ 'flat': flat as unknown as GlassDateTokens,
26
+ 'plain': plain as unknown as GlassDateTokens,
27
+ };
28
+
29
+ const SINGLE_MODE_THEMES: Set<ThemeKey> = new Set(['toon', 'flat', 'plain']);
30
+
31
+ function deepMerge<T extends Record<string, unknown>>(base: T, override: Partial<Record<string, unknown>>): T {
32
+ const result = { ...base };
33
+ for (const key of Object.keys(override)) {
34
+ const ov = override[key];
35
+ const bv = result[key as keyof T];
36
+ if (
37
+ ov !== undefined &&
38
+ ov !== null &&
39
+ typeof ov === 'object' &&
40
+ !Array.isArray(ov) &&
41
+ typeof bv === 'object' &&
42
+ bv !== null
43
+ ) {
44
+ (result as Record<string, unknown>)[key] = deepMerge(
45
+ bv as Record<string, unknown>,
46
+ ov as Record<string, unknown>,
47
+ );
48
+ } else if (ov !== undefined) {
49
+ (result as Record<string, unknown>)[key] = ov;
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ function replaceAccent(tokens: GlassDateTokens, resolvedAccent: string): GlassDateTokens {
56
+ const result = JSON.parse(JSON.stringify(tokens)) as GlassDateTokens;
57
+
58
+ const replace = (obj: Record<string, unknown>) => {
59
+ for (const key of Object.keys(obj)) {
60
+ const val = obj[key];
61
+ if (val === 'accent') {
62
+ obj[key] = resolvedAccent;
63
+ } else if (typeof val === 'object' && val !== null) {
64
+ replace(val as Record<string, unknown>);
65
+ }
66
+ }
67
+ };
68
+
69
+ replace(result as unknown as Record<string, unknown>);
70
+ return result;
71
+ }
72
+
73
+ export function mapDayShapeToRadius(dayShape: DayShape): number {
74
+ switch (dayShape) {
75
+ case 'circle':
76
+ return 9999;
77
+ case 'round-square':
78
+ return 14;
79
+ case 'subtle':
80
+ return 10;
81
+ }
82
+ }
83
+
84
+ export function resolveTokens(
85
+ theme: ThemeKey,
86
+ colorScheme: ColorScheme,
87
+ accent: string | undefined,
88
+ tokenOverrides: DeepPartial<GlassDateTokens> | undefined,
89
+ ): GlassDateTokens {
90
+ // Step 1 / 1a — Load base theme
91
+ let tokens: GlassDateTokens;
92
+
93
+ if (theme === 'mono') {
94
+ const resolvedAccent =
95
+ accent !== undefined && validateHexColour(accent) ? accent : MONO_DEFAULT_ACCENT;
96
+ tokens = computeMonoTokens(resolvedAccent);
97
+ // Mono outputs use accent directly — no "accent" keyword to replace.
98
+ // Jump to Step 4 (dayShape handled by caller) then Step 5.
99
+ if (tokenOverrides !== undefined) {
100
+ tokens = deepMerge(
101
+ tokens as unknown as Record<string, unknown>,
102
+ tokenOverrides as unknown as Partial<Record<string, unknown>>,
103
+ ) as unknown as GlassDateTokens;
104
+ }
105
+ return tokens;
106
+ }
107
+
108
+ if (SINGLE_MODE_THEMES.has(theme)) {
109
+ const loaded = THEME_MAP[theme];
110
+ if (!loaded) {
111
+ throw new Error(`Unknown theme: ${theme}`);
112
+ }
113
+ tokens = JSON.parse(JSON.stringify(loaded)) as GlassDateTokens;
114
+ } else {
115
+ const key = `${theme}.${colorScheme}`;
116
+ const loaded = THEME_MAP[key];
117
+ if (!loaded) {
118
+ throw new Error(`Unknown theme/colorScheme: ${key}`);
119
+ }
120
+ tokens = JSON.parse(JSON.stringify(loaded)) as GlassDateTokens;
121
+ }
122
+
123
+ // Step 2 — Resolve accent
124
+ const resolvedAccent =
125
+ accent !== undefined && validateHexColour(accent) ? accent : tokens.meta.defaultAccent;
126
+
127
+ // Step 3 — Substitute 'accent' keyword
128
+ tokens = replaceAccent(tokens, resolvedAccent);
129
+
130
+ // Step 4 — dayShape mapping handled by caller (mapDayShapeToRadius)
131
+
132
+ // Step 5 — Deep-merge partial token overrides
133
+ if (tokenOverrides !== undefined) {
134
+ tokens = deepMerge(
135
+ tokens as unknown as Record<string, unknown>,
136
+ tokenOverrides as unknown as Partial<Record<string, unknown>>,
137
+ ) as unknown as GlassDateTokens;
138
+ }
139
+
140
+ return tokens;
141
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * GlassDateTokens — generated from packages/tokens/schema/tokens.schema.json
3
+ * Do not edit by hand.
4
+ */
5
+
6
+ export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
7
+
8
+ export interface SceneTokens {
9
+ backgroundColour: string;
10
+ }
11
+
12
+ export interface ContainerTokens {
13
+ backgroundColour: string;
14
+ borderColour: string;
15
+ borderWidth: number;
16
+ cornerRadius: number;
17
+ shadowColour: string;
18
+ shadowOffsetX: number;
19
+ shadowOffsetY: number;
20
+ shadowBlurRadius: number;
21
+ shadowOpacity: number;
22
+ blurRadius: number;
23
+ }
24
+
25
+ export interface PillTokens {
26
+ backgroundColour: string;
27
+ borderColour: string;
28
+ borderWidth: number;
29
+ cornerRadius: number;
30
+ textColour: string;
31
+ fontWeight: number;
32
+ hoverBackgroundColour: string;
33
+ todayBackgroundColour: string;
34
+ todayBorderColour: string;
35
+ todayTextColour: string;
36
+ }
37
+
38
+ export interface PopupTokens {
39
+ backgroundColour: string;
40
+ borderColour: string;
41
+ borderWidth: number;
42
+ cornerRadius: number;
43
+ itemTextColour: string;
44
+ itemFontWeight: number;
45
+ itemHoverBackgroundColour: string;
46
+ itemActiveBackgroundColour: string;
47
+ itemActiveTextColour: string;
48
+ itemSelectedBackgroundColour: string;
49
+ itemSelectedTextColour: string;
50
+ itemDisabledOpacity: number;
51
+ }
52
+
53
+ export interface NavTokens {
54
+ backgroundColour: string;
55
+ borderColour: string;
56
+ borderWidth: number;
57
+ cornerRadius: number;
58
+ arrowColour: string;
59
+ hoverBackgroundColour: string;
60
+ disabledOpacity: number;
61
+ }
62
+
63
+ export interface GridTokens {
64
+ weekdayLabelColour: string;
65
+ weekdayFontWeight: number;
66
+ dayTextColour: string;
67
+ dayFontWeight: number;
68
+ dayHoverBackgroundColour: string;
69
+ daySelectedBackgroundColour: string;
70
+ daySelectedTextColour: string;
71
+ dayTodayFontWeight: number;
72
+ dayTodayDotColour: string;
73
+ dayOutOfRangeOpacity: number;
74
+ dayAdjacentMonthOpacity: number;
75
+ }
76
+
77
+ export interface FooterTokens {
78
+ cancelBackgroundColour: string;
79
+ cancelBorderColour: string;
80
+ cancelBorderWidth: number;
81
+ cancelTextColour: string;
82
+ cancelCornerRadius: number;
83
+ cancelHoverBackgroundColour: string;
84
+ confirmBackgroundColour: string;
85
+ confirmBorderColour: string;
86
+ confirmBorderWidth: number;
87
+ confirmTextColour: string;
88
+ confirmCornerRadius: number;
89
+ confirmHoverBackgroundColour: string;
90
+ disabledOpacity: number;
91
+ selectedDateTextColour: string;
92
+ }
93
+
94
+ export interface MetaTokens {
95
+ name: string;
96
+ mode: 'dark' | 'light' | 'single';
97
+ defaultAccent: string;
98
+ }
99
+
100
+ export interface GlassDateTokens {
101
+ scene: SceneTokens;
102
+ container: ContainerTokens;
103
+ pill: PillTokens;
104
+ popup: PopupTokens;
105
+ nav: NavTokens;
106
+ grid: GridTokens;
107
+ footer: FooterTokens;
108
+ meta: MetaTokens;
109
+ }
110
+
111
+ export type ThemeKey = 'default' | 'ios-liquid' | 'material3' | 'toon' | 'flat' | 'plain' | 'mono';
112
+
113
+ export type ConstraintPreset = 'none' | 'birthday' | 'adult' | 'future' | 'custom';
114
+
115
+ export type DayShape = 'circle' | 'round-square' | 'subtle';
116
+
117
+ export type OpenDropdown = 'none' | 'month' | 'year';
@@ -0,0 +1,32 @@
1
+ const HEX6_REGEX = /^#[0-9A-Fa-f]{6}$/;
2
+ const HEX8_REGEX = /^#[0-9A-Fa-f]{8}$/;
3
+
4
+ export function validateHexColour(s: string): boolean {
5
+ return HEX6_REGEX.test(s) || HEX8_REGEX.test(s);
6
+ }
7
+
8
+ export function hexToRgba(s: string): { r: number; g: number; b: number; a: number } {
9
+ const h = s.replace('#', '');
10
+ const r = parseInt(h.slice(0, 2), 16);
11
+ const g = parseInt(h.slice(2, 4), 16);
12
+ const b = parseInt(h.slice(4, 6), 16);
13
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255;
14
+ return { r, g, b, a };
15
+ }
16
+
17
+ function relativeLuminance(hex: string): number {
18
+ const { r, g, b } = hexToRgba(hex);
19
+ const linearise = (c: number) => {
20
+ const s = c / 255;
21
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
22
+ };
23
+ return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b);
24
+ }
25
+
26
+ export function contrastRatio(fg: string, bg: string): number {
27
+ const l1 = relativeLuminance(fg);
28
+ const l2 = relativeLuminance(bg);
29
+ const lighter = Math.max(l1, l2);
30
+ const darker = Math.min(l1, l2);
31
+ return (lighter + 0.05) / (darker + 0.05);
32
+ }