@ecohouse/ui 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,286 @@
1
+ /**
2
+ * EcoHouse design tokens — aligned with the shared design system
3
+ * (navy / storm gray / slate-blue palette).
4
+ */
5
+
6
+ /** Storm Gray scale — Figma steps 0 → 8.5 */
7
+ export const stormGray = {
8
+ "0": "#fbfcfc",
9
+ "0.5": "#eceef0",
10
+ "1": "#dadde0",
11
+ "1.5": "#c7cdd1",
12
+ "2": "#b5bcc1",
13
+ "3": "#8f9aa3",
14
+ "4": "#6a7984",
15
+ "5": "#455765",
16
+ "6": "#374651",
17
+ "7": "#29343d",
18
+ "8": "#1c2328",
19
+ "8.5": "#151a1e",
20
+ } as const;
21
+
22
+ export type StormGrayStep = keyof typeof stormGray;
23
+
24
+ /** Navy scale — Figma steps 0 → 8.5 */
25
+ export const navy = {
26
+ "0": "#f6fafe",
27
+ "0.5": "#dee5eb",
28
+ "1": "#c6d0d8",
29
+ "1.5": "#afbac5",
30
+ "2": "#97a5b2",
31
+ "3": "#677b8c",
32
+ "4": "#385066",
33
+ "5": "#082640",
34
+ "6": "#061e33",
35
+ "7": "#051726",
36
+ "8": "#030f1a",
37
+ "8.5": "#020b13",
38
+ } as const;
39
+
40
+ export type NavyStep = keyof typeof navy;
41
+
42
+ /** Ruby Red scale — Figma steps 0 → 8.5 */
43
+ export const rubyRed = {
44
+ "0": "#fff5f6",
45
+ "0.5": "#f7dddf",
46
+ "1": "#efc4c8",
47
+ "1.5": "#e7acb1",
48
+ "2": "#df939a",
49
+ "3": "#ce626d",
50
+ "4": "#be313f",
51
+ "5": "#ae0011",
52
+ "6": "#8b000e",
53
+ "7": "#68000a",
54
+ "8": "#460007",
55
+ "8.5": "#340005",
56
+ } as const;
57
+
58
+ export type RubyRedStep = keyof typeof rubyRed;
59
+
60
+ /** Emerald Green scale — Figma steps 0 → 8.5 */
61
+ export const emeraldGreen = {
62
+ "0": "#f0fbf5",
63
+ "0.5": "#dcf2e5",
64
+ "1": "#c8e8d5",
65
+ "1.5": "#b4dfc5",
66
+ "2": "#a0d5b5",
67
+ "3": "#77c394",
68
+ "4": "#4fb074",
69
+ "5": "#279d54",
70
+ "6": "#1f7e43",
71
+ "7": "#175e32",
72
+ "8": "#103f22",
73
+ "8.5": "#0c2f19",
74
+ } as const;
75
+
76
+ export type EmeraldGreenStep = keyof typeof emeraldGreen;
77
+
78
+ /** Figma brand palette — primary swatch per color family */
79
+ export const brand = {
80
+ slateBlue: "#182e3c",
81
+ stormGray: stormGray["5"],
82
+ navy: navy["5"],
83
+ rubyRed: rubyRed["5"],
84
+ emeraldGreen: emeraldGreen["5"],
85
+ } as const;
86
+
87
+ export const colors = {
88
+ // Brand colors
89
+ slateBlue: brand.slateBlue,
90
+
91
+ // Emerald Green — Figma scale + brand swatch
92
+ emeraldGreen0: emeraldGreen["0"],
93
+ emeraldGreen50: emeraldGreen["0.5"],
94
+ emeraldGreen100: emeraldGreen["1"],
95
+ emeraldGreen150: emeraldGreen["1.5"],
96
+ emeraldGreen200: emeraldGreen["2"],
97
+ emeraldGreen300: emeraldGreen["3"],
98
+ emeraldGreen400: emeraldGreen["4"],
99
+ emeraldGreen500: emeraldGreen["5"],
100
+ emeraldGreen600: emeraldGreen["6"],
101
+ emeraldGreen700: emeraldGreen["7"],
102
+ emeraldGreen800: emeraldGreen["8"],
103
+ emeraldGreen850: emeraldGreen["8.5"],
104
+
105
+ /** Primary brand emerald green — Figma Emerald Green 5 */
106
+ emeraldGreen: brand.emeraldGreen,
107
+
108
+ // Ruby Red — Figma scale + brand swatch
109
+ rubyRed0: rubyRed["0"],
110
+ rubyRed50: rubyRed["0.5"],
111
+ rubyRed100: rubyRed["1"],
112
+ rubyRed150: rubyRed["1.5"],
113
+ rubyRed200: rubyRed["2"],
114
+ rubyRed300: rubyRed["3"],
115
+ rubyRed400: rubyRed["4"],
116
+ rubyRed500: rubyRed["5"],
117
+ rubyRed600: rubyRed["6"],
118
+ rubyRed700: rubyRed["7"],
119
+ rubyRed800: rubyRed["8"],
120
+ rubyRed850: rubyRed["8.5"],
121
+
122
+ /** Primary brand ruby red — Figma Ruby Red 5 */
123
+ rubyRed: brand.rubyRed,
124
+
125
+ // Navy — Figma scale + brand swatch
126
+ navy0: navy["0"],
127
+ navy50: navy["0.5"],
128
+ navy100: navy["1"],
129
+ navy150: navy["1.5"],
130
+ navy200: navy["2"],
131
+ navy300: navy["3"],
132
+ navy400: navy["4"],
133
+ navy500: navy["5"],
134
+ navy600: navy["6"],
135
+ navy700: navy["7"],
136
+ navy800: navy["8"],
137
+ navy850: navy["8.5"],
138
+
139
+ /** Primary brand navy — Figma Navy 5 */
140
+ navy: brand.navy,
141
+
142
+ // Storm Gray — Figma scale + brand swatch
143
+ stormGray0: stormGray["0"],
144
+ stormGray50: stormGray["0.5"],
145
+ stormGray100: stormGray["1"],
146
+ stormGray150: stormGray["1.5"],
147
+ stormGray200: stormGray["2"],
148
+ stormGray300: stormGray["3"],
149
+ stormGray400: stormGray["4"],
150
+ stormGray500: stormGray["5"],
151
+ /** Figma brand swatch "Storm Gray" — same as `stormGray500` */
152
+ stormGrayBrand: brand.stormGray,
153
+ stormGray600: stormGray["6"],
154
+ stormGray700: stormGray["7"],
155
+ stormGray800: stormGray["8"],
156
+ stormGray850: stormGray["8.5"],
157
+
158
+ /** @deprecated Use `stormGray0` */
159
+ storm0: stormGray["0"],
160
+ /** @deprecated Use `stormGray50` */
161
+ storm50: stormGray["0.5"],
162
+ /** @deprecated Use `stormGray200` */
163
+ storm200: stormGray["2"],
164
+ /** @deprecated Use `stormGray300` */
165
+ storm300: stormGray["3"],
166
+ /** @deprecated Use `stormGray500` */
167
+ storm500: stormGray["5"],
168
+ /** @deprecated Use `stormGray700` */
169
+ storm700: stormGray["7"],
170
+ /** @deprecated Use `stormGray850` */
171
+ storm900: stormGray["8.5"],
172
+
173
+ countrySelectorSelectedBg: `${stormGray["0.5"]}99`,
174
+
175
+ white: "#ffffff",
176
+ error: "#dc2626",
177
+ errorDark: "#b3261e",
178
+ errorLight: "#fee2e2",
179
+ /** @deprecated Use `rubyRed` */
180
+ inputError: brand.rubyRed,
181
+ inputOutlineFocus: navy["0.5"],
182
+ inputOutlineError: rubyRed["0.5"],
183
+ warning: "#92400e",
184
+ warningBg: "#fef3c7",
185
+ success: "#28a745",
186
+ successMuted: "#9fd4a8",
187
+ /** @deprecated Use `emeraldGreen` */
188
+ green: brand.emeraldGreen,
189
+ transparent: "transparent",
190
+ } as const;
191
+
192
+ export const radii = {
193
+ sm: 8,
194
+ md: 12,
195
+ lg: 16,
196
+ xl: 20,
197
+ full: 9999,
198
+ } as const;
199
+
200
+ export const spacing = {
201
+ xs: 4,
202
+ sm: 8,
203
+ md: 12,
204
+ lg: 16,
205
+ xl: 24,
206
+ xxl: 32,
207
+ } as const;
208
+
209
+ export const fontSize = {
210
+ xs: 11,
211
+ sm: 12,
212
+ md: 14,
213
+ base: 16,
214
+ lg: 18,
215
+ xl: 24,
216
+ xxl: 32,
217
+ } as const;
218
+
219
+ /**
220
+ * Canonical Homepad type scale (matches `.typography-*` in `src/web/styles/typography.css`).
221
+ * font-size / line-height pairs:
222
+ * 12/16 · 14/19 · 18/24 · 20/27 · 24/25 · 32/34
223
+ */
224
+ export const typographyScale = {
225
+ 12: { fontSize: 12, lineHeight: 16 },
226
+ 14: { fontSize: 14, lineHeight: 19 },
227
+ 18: { fontSize: 18, lineHeight: 24 },
228
+ 20: { fontSize: 20, lineHeight: 27 },
229
+ 24: { fontSize: 24, lineHeight: 25 },
230
+ 32: { fontSize: 32, lineHeight: 34 },
231
+ } as const;
232
+
233
+ export const fontWeight = {
234
+ regular: "400" as const,
235
+ medium: "500" as const,
236
+ semibold: "600" as const,
237
+ bold: "700" as const,
238
+ };
239
+
240
+ export const fonts = {
241
+ sans: "Noto Sans Armenian",
242
+ } as const;
243
+
244
+ /** Input label typography — Medium, typography-12 scale. */
245
+ export const labelTypography = {
246
+ fontFamily: fonts.sans,
247
+ fontSize: typographyScale[12].fontSize,
248
+ fontWeight: fontWeight.medium,
249
+ lineHeight: typographyScale[12].lineHeight,
250
+ letterSpacing: 0,
251
+ } as const;
252
+
253
+ /** Button label typography — SemiBold. */
254
+ export const buttonTypography = {
255
+ lg: {
256
+ fontFamily: fonts.sans,
257
+ fontSize: typographyScale[14].fontSize,
258
+ fontWeight: fontWeight.semibold,
259
+ lineHeight: typographyScale[14].lineHeight,
260
+ letterSpacing: 0,
261
+ },
262
+ sm: {
263
+ fontFamily: fonts.sans,
264
+ fontSize: typographyScale[12].fontSize,
265
+ fontWeight: fontWeight.semibold,
266
+ lineHeight: typographyScale[12].lineHeight,
267
+ letterSpacing: 0,
268
+ },
269
+ } as const;
270
+
271
+ export const shadows = {
272
+ card: {
273
+ shadowColor: colors.navy,
274
+ shadowOffset: { width: 0, height: 4 },
275
+ shadowOpacity: 0.05,
276
+ shadowRadius: 20,
277
+ elevation: 2,
278
+ },
279
+ cardLg: {
280
+ shadowColor: colors.navy,
281
+ shadowOffset: { width: 0, height: 4 },
282
+ shadowOpacity: 0.1,
283
+ shadowRadius: 20,
284
+ elevation: 4,
285
+ },
286
+ } as const;
@@ -0,0 +1,83 @@
1
+ import { useLayoutEffect } from "react";
2
+ import { Platform } from "react-native";
3
+
4
+ interface ClassListElement {
5
+ classList: {
6
+ add: (...classes: string[]) => void;
7
+ remove: (...classes: string[]) => void;
8
+ };
9
+ }
10
+
11
+ function hasClassList(node: unknown): node is ClassListElement {
12
+ return (
13
+ typeof node === "object" &&
14
+ node !== null &&
15
+ "classList" in node &&
16
+ typeof (node as ClassListElement).classList?.add === "function"
17
+ );
18
+ }
19
+
20
+ function resolveWebElement(ref: React.RefObject<unknown>): ClassListElement | null {
21
+ const node = ref.current;
22
+ if (!node) return null;
23
+
24
+ if (hasClassList(node)) return node;
25
+
26
+ const host = node as {
27
+ _touchableNode?: unknown;
28
+ getScrollableNode?: () => unknown;
29
+ };
30
+
31
+ if (hasClassList(host._touchableNode)) return host._touchableNode;
32
+
33
+ if (typeof host.getScrollableNode === "function") {
34
+ const scrollNode = host.getScrollableNode();
35
+ if (hasClassList(scrollNode)) return scrollNode;
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Applies CSS class names to the underlying DOM node on web.
43
+ * Keeps TouchableOpacity/Text as the render path so default RN styles stay intact.
44
+ *
45
+ * Retries via rAF when the host node is not ready yet — otherwise classes like
46
+ * `h-13` / `w-full` would only land after a later re-render (e.g. typing in a form),
47
+ * which visibly changes button size.
48
+ */
49
+ export function useApplyWebClassName(
50
+ ref: React.RefObject<unknown>,
51
+ className?: string,
52
+ enabled = true,
53
+ ): void {
54
+ useLayoutEffect(() => {
55
+ if (!enabled || Platform.OS !== "web" || !className?.trim()) return;
56
+
57
+ let cancelled = false;
58
+ let rafId = 0;
59
+ let appliedElement: ClassListElement | null = null;
60
+ const classes = className.trim().split(/\s+/);
61
+
62
+ const apply = () => {
63
+ if (cancelled) return;
64
+
65
+ const element = resolveWebElement(ref);
66
+ if (!element) {
67
+ rafId = requestAnimationFrame(apply);
68
+ return;
69
+ }
70
+
71
+ appliedElement = element;
72
+ element.classList.add(...classes);
73
+ };
74
+
75
+ apply();
76
+
77
+ return () => {
78
+ cancelled = true;
79
+ if (rafId) cancelAnimationFrame(rafId);
80
+ appliedElement?.classList.remove(...classes);
81
+ };
82
+ }, [ref, className, enabled]);
83
+ }
@@ -0,0 +1,99 @@
1
+ @layer components {
2
+ .typography-12 {
3
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
4
+ font-size: 12px;
5
+ line-height: 16px;
6
+ letter-spacing: 0;
7
+ }
8
+
9
+ .typography-14 {
10
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
11
+ font-size: 14px;
12
+ line-height: 19px;
13
+ letter-spacing: 0;
14
+ }
15
+
16
+ .typography-18 {
17
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
18
+ font-size: 18px;
19
+ line-height: 24px;
20
+ letter-spacing: 0;
21
+ }
22
+
23
+ .typography-20 {
24
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
25
+ font-size: 20px;
26
+ line-height: 27px;
27
+ letter-spacing: 0;
28
+ }
29
+
30
+ .typography-24 {
31
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
32
+ font-size: 24px;
33
+ line-height: 25px;
34
+ letter-spacing: 0;
35
+ }
36
+
37
+ .typography-32 {
38
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
39
+ font-size: 32px;
40
+ line-height: 34px;
41
+ letter-spacing: 0;
42
+ }
43
+
44
+ .typography-body {
45
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
46
+ font-size: 14px;
47
+ line-height: 19px;
48
+ letter-spacing: 0;
49
+ font-weight: 400;
50
+ }
51
+
52
+ .typography-body-medium {
53
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
54
+ font-size: 14px;
55
+ line-height: 19px;
56
+ letter-spacing: 0;
57
+ font-weight: 500;
58
+ }
59
+
60
+ .typography-caption {
61
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
62
+ font-size: 12px;
63
+ line-height: 16px;
64
+ letter-spacing: 0;
65
+ font-weight: 400;
66
+ }
67
+
68
+ .typography-title-sm {
69
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
70
+ font-size: 18px;
71
+ line-height: 24px;
72
+ letter-spacing: 0;
73
+ font-weight: 700;
74
+ }
75
+
76
+ .typography-title {
77
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
78
+ font-size: 20px;
79
+ line-height: 27px;
80
+ letter-spacing: 0;
81
+ font-weight: 700;
82
+ }
83
+
84
+ .typography-title-lg {
85
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
86
+ font-size: 24px;
87
+ line-height: 25px;
88
+ letter-spacing: 0;
89
+ font-weight: 700;
90
+ }
91
+
92
+ .typography-display {
93
+ font-family: var(--font-sans, "Noto Sans Armenian", ui-sans-serif, system-ui, sans-serif);
94
+ font-size: 32px;
95
+ line-height: 34px;
96
+ letter-spacing: 0;
97
+ font-weight: 700;
98
+ }
99
+ }