@lotics/ui 42.0.0 → 42.3.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/docs/catalog.md +15 -0
- package/docs/composition.md +23 -2
- package/package.json +10 -3
- package/src/avatar.tsx +6 -1
- package/src/avatar.web.tsx +6 -1
- package/src/avatar_size.ts +10 -1
- package/src/color_tokens.ts +422 -0
- package/src/colors.ts +14 -383
- package/src/colors.web.ts +43 -0
- package/src/summary.tsx +217 -0
- package/src/summary_line.tsx +23 -0
- package/src/theme.tsx +13 -50
- package/src/theme.web.tsx +64 -0
- package/src/theme_context.ts +45 -0
package/src/colors.ts
CHANGED
|
@@ -1,386 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
black: "rgba(0,0,0,1)",
|
|
3
|
-
white: "rgba(255,255,255,1)",
|
|
4
|
-
|
|
5
|
-
// We use RGBA so that we can use React Native Animations with colors
|
|
6
|
-
red: {
|
|
7
|
-
50: "rgba(254, 242, 242, 1)",
|
|
8
|
-
100: "rgba(254, 226, 226, 1)",
|
|
9
|
-
200: "rgba(254, 202, 202, 1)",
|
|
10
|
-
300: "rgba(252, 165, 165, 1)",
|
|
11
|
-
400: "rgba(248, 113, 113, 1)",
|
|
12
|
-
500: "rgba(239, 68, 68, 1)",
|
|
13
|
-
600: "rgba(220, 38, 38, 1)",
|
|
14
|
-
700: "rgba(185, 28, 28, 1)",
|
|
15
|
-
800: "rgba(153, 27, 27, 1)",
|
|
16
|
-
900: "rgba(127, 29, 29, 1)",
|
|
17
|
-
950: "rgba(69, 10, 10, 1)",
|
|
18
|
-
},
|
|
19
|
-
orange: {
|
|
20
|
-
50: "rgba(255, 247, 237, 1)",
|
|
21
|
-
100: "rgba(255, 237, 213, 1)",
|
|
22
|
-
200: "rgba(254, 215, 170, 1)",
|
|
23
|
-
300: "rgba(253, 186, 116, 1)",
|
|
24
|
-
400: "rgba(251, 146, 60, 1)",
|
|
25
|
-
500: "rgba(249, 115, 22, 1)",
|
|
26
|
-
600: "rgba(234, 88, 12, 1)",
|
|
27
|
-
700: "rgba(194, 65, 12, 1)",
|
|
28
|
-
800: "rgba(154, 52, 18, 1)",
|
|
29
|
-
900: "rgba(124, 45, 18, 1)",
|
|
30
|
-
950: "rgba(67, 20, 7, 1)",
|
|
31
|
-
},
|
|
32
|
-
amber: {
|
|
33
|
-
50: "rgba(255, 251, 235, 1)",
|
|
34
|
-
100: "rgba(254, 243, 199, 1)",
|
|
35
|
-
200: "rgba(253, 230, 138, 1)",
|
|
36
|
-
300: "rgba(252, 211, 77, 1)",
|
|
37
|
-
400: "rgba(251, 191, 36, 1)",
|
|
38
|
-
500: "rgba(245, 158, 11, 1)",
|
|
39
|
-
600: "rgba(217, 119, 6, 1)",
|
|
40
|
-
700: "rgba(180, 83, 9, 1)",
|
|
41
|
-
800: "rgba(146, 64, 14, 1)",
|
|
42
|
-
900: "rgba(120, 53, 15, 1)",
|
|
43
|
-
950: "rgba(69, 26, 3, 1)",
|
|
44
|
-
},
|
|
45
|
-
yellow: {
|
|
46
|
-
50: "rgba(254, 252, 232, 1)",
|
|
47
|
-
100: "rgba(254, 249, 195, 1)",
|
|
48
|
-
200: "rgba(254, 240, 138, 1)",
|
|
49
|
-
300: "rgba(253, 224, 71, 1)",
|
|
50
|
-
400: "rgba(250, 204, 21, 1)",
|
|
51
|
-
500: "rgba(234, 179, 8, 1)",
|
|
52
|
-
600: "rgba(202, 138, 4, 1)",
|
|
53
|
-
700: "rgba(161, 98, 7, 1)",
|
|
54
|
-
800: "rgba(133, 77, 14, 1)",
|
|
55
|
-
900: "rgba(113, 63, 18, 1)",
|
|
56
|
-
950: "rgba(66, 32, 6, 1)",
|
|
57
|
-
},
|
|
58
|
-
lime: {
|
|
59
|
-
50: "rgba(247, 254, 231, 1)",
|
|
60
|
-
100: "rgba(236, 252, 203, 1)",
|
|
61
|
-
200: "rgba(217, 249, 157, 1)",
|
|
62
|
-
300: "rgba(190, 242, 100, 1)",
|
|
63
|
-
400: "rgba(163, 230, 53, 1)",
|
|
64
|
-
500: "rgba(132, 204, 22, 1)",
|
|
65
|
-
600: "rgba(101, 163, 13, 1)",
|
|
66
|
-
700: "rgba(77, 124, 15, 1)",
|
|
67
|
-
800: "rgba(63, 98, 18, 1)",
|
|
68
|
-
900: "rgba(54, 83, 20, 1)",
|
|
69
|
-
950: "rgba(26, 46, 5, 1)",
|
|
70
|
-
},
|
|
71
|
-
green: {
|
|
72
|
-
50: "rgba(240, 253, 244, 1)",
|
|
73
|
-
100: "rgba(220, 252, 231, 1)",
|
|
74
|
-
200: "rgba(187, 247, 208, 1)",
|
|
75
|
-
300: "rgba(134, 239, 172, 1)",
|
|
76
|
-
400: "rgba(74, 222, 128, 1)",
|
|
77
|
-
500: "rgba(34, 197, 94, 1)",
|
|
78
|
-
600: "rgba(22, 163, 74, 1)",
|
|
79
|
-
700: "rgba(21, 128, 61, 1)",
|
|
80
|
-
800: "rgba(22, 101, 52, 1)",
|
|
81
|
-
900: "rgba(20, 83, 45, 1)",
|
|
82
|
-
950: "rgba(5, 46, 22, 1)",
|
|
83
|
-
},
|
|
84
|
-
emerald: {
|
|
85
|
-
50: "rgba(236, 253, 245, 1)",
|
|
86
|
-
100: "rgba(209, 250, 229, 1)",
|
|
87
|
-
200: "rgba(167, 243, 208, 1)",
|
|
88
|
-
300: "rgba(110, 231, 183, 1)",
|
|
89
|
-
400: "rgba(52, 211, 153, 1)",
|
|
90
|
-
500: "rgba(16, 185, 129, 1)",
|
|
91
|
-
600: "rgba(5, 150, 105, 1)",
|
|
92
|
-
700: "rgba(4, 120, 87, 1)",
|
|
93
|
-
800: "rgba(6, 95, 70, 1)",
|
|
94
|
-
900: "rgba(6, 78, 59, 1)",
|
|
95
|
-
950: "rgba(2, 44, 34, 1)",
|
|
96
|
-
},
|
|
97
|
-
teal: {
|
|
98
|
-
50: "rgba(240, 253, 250, 1)",
|
|
99
|
-
100: "rgba(204, 251, 241, 1)",
|
|
100
|
-
200: "rgba(153, 246, 228, 1)",
|
|
101
|
-
300: "rgba(94, 234, 212, 1)",
|
|
102
|
-
400: "rgba(45, 212, 191, 1)",
|
|
103
|
-
500: "rgba(20, 184, 166, 1)",
|
|
104
|
-
600: "rgba(13, 148, 136, 1)",
|
|
105
|
-
700: "rgba(15, 118, 110, 1)",
|
|
106
|
-
800: "rgba(17, 94, 89, 1)",
|
|
107
|
-
900: "rgba(19, 78, 74, 1)",
|
|
108
|
-
950: "rgba(4, 47, 46, 1)",
|
|
109
|
-
},
|
|
110
|
-
cyan: {
|
|
111
|
-
50: "rgba(236, 254, 255, 1)",
|
|
112
|
-
100: "rgba(207, 250, 254, 1)",
|
|
113
|
-
200: "rgba(165, 243, 252, 1)",
|
|
114
|
-
300: "rgba(103, 232, 249, 1)",
|
|
115
|
-
400: "rgba(34, 211, 238, 1)",
|
|
116
|
-
500: "rgba(6, 182, 212, 1)",
|
|
117
|
-
600: "rgba(8, 145, 178, 1)",
|
|
118
|
-
700: "rgba(14, 116, 144, 1)",
|
|
119
|
-
800: "rgba(21, 94, 117, 1)",
|
|
120
|
-
900: "rgba(22, 78, 99, 1)",
|
|
121
|
-
950: "rgba(8, 51, 68, 1)",
|
|
122
|
-
},
|
|
123
|
-
sky: {
|
|
124
|
-
50: "rgba(240, 249, 255, 1)",
|
|
125
|
-
100: "rgba(224, 242, 254, 1)",
|
|
126
|
-
200: "rgba(186, 230, 253, 1)",
|
|
127
|
-
300: "rgba(125, 211, 252, 1)",
|
|
128
|
-
400: "rgba(56, 189, 248, 1)",
|
|
129
|
-
500: "rgba(14, 165, 233, 1)",
|
|
130
|
-
600: "rgba(2, 132, 199, 1)",
|
|
131
|
-
700: "rgba(3, 105, 161, 1)",
|
|
132
|
-
800: "rgba(7, 89, 133, 1)",
|
|
133
|
-
900: "rgba(12, 74, 110, 1)",
|
|
134
|
-
950: "rgba(8, 47, 73, 1)",
|
|
135
|
-
},
|
|
136
|
-
blue: {
|
|
137
|
-
50: "rgba(239, 246, 255, 1)",
|
|
138
|
-
100: "rgba(219, 234, 254, 1)",
|
|
139
|
-
200: "rgba(191, 219, 254, 1)",
|
|
140
|
-
300: "rgba(147, 197, 253, 1)",
|
|
141
|
-
400: "rgba(96, 165, 250, 1)",
|
|
142
|
-
500: "rgba(59, 130, 246, 1)",
|
|
143
|
-
600: "rgba(37, 99, 235, 1)",
|
|
144
|
-
700: "rgba(29, 78, 216, 1)",
|
|
145
|
-
800: "rgba(30, 64, 175, 1)",
|
|
146
|
-
900: "rgba(30, 58, 138, 1)",
|
|
147
|
-
950: "rgba(23, 37, 84, 1)",
|
|
148
|
-
},
|
|
149
|
-
indigo: {
|
|
150
|
-
50: "rgba(238, 242, 255, 1)",
|
|
151
|
-
100: "rgba(224, 231, 255, 1)",
|
|
152
|
-
200: "rgba(199, 210, 254, 1)",
|
|
153
|
-
300: "rgba(165, 180, 252, 1)",
|
|
154
|
-
400: "rgba(129, 140, 248, 1)",
|
|
155
|
-
500: "rgba(99, 102, 241, 1)",
|
|
156
|
-
600: "rgba(79, 70, 229, 1)",
|
|
157
|
-
700: "rgba(67, 56, 202, 1)",
|
|
158
|
-
800: "rgba(55, 48, 163, 1)",
|
|
159
|
-
900: "rgba(49, 46, 129, 1)",
|
|
160
|
-
950: "rgba(30, 27, 75, 1)",
|
|
161
|
-
},
|
|
162
|
-
violet: {
|
|
163
|
-
50: "rgba(245, 243, 255, 1)",
|
|
164
|
-
100: "rgba(237, 233, 254, 1)",
|
|
165
|
-
200: "rgba(221, 214, 254, 1)",
|
|
166
|
-
300: "rgba(196, 181, 253, 1)",
|
|
167
|
-
400: "rgba(167, 139, 250, 1)",
|
|
168
|
-
500: "rgba(139, 92, 246, 1)",
|
|
169
|
-
600: "rgba(124, 58, 237, 1)",
|
|
170
|
-
700: "rgba(109, 40, 217, 1)",
|
|
171
|
-
800: "rgba(91, 33, 182, 1)",
|
|
172
|
-
900: "rgba(76, 29, 149, 1)",
|
|
173
|
-
950: "rgba(46, 16, 101, 1)",
|
|
174
|
-
},
|
|
175
|
-
purple: {
|
|
176
|
-
50: "rgba(250, 245, 255, 1)",
|
|
177
|
-
100: "rgba(243, 232, 255, 1)",
|
|
178
|
-
200: "rgba(233, 213, 255, 1)",
|
|
179
|
-
300: "rgba(216, 180, 254, 1)",
|
|
180
|
-
400: "rgba(192, 132, 252, 1)",
|
|
181
|
-
500: "rgba(168, 85, 247, 1)",
|
|
182
|
-
600: "rgba(147, 51, 234, 1)",
|
|
183
|
-
700: "rgba(126, 34, 206, 1)",
|
|
184
|
-
800: "rgba(107, 33, 168, 1)",
|
|
185
|
-
900: "rgba(88, 28, 135, 1)",
|
|
186
|
-
950: "rgba(59, 7, 100, 1)",
|
|
187
|
-
},
|
|
188
|
-
fuchsia: {
|
|
189
|
-
50: "rgba(253, 244, 255, 1)",
|
|
190
|
-
100: "rgba(250, 232, 255, 1)",
|
|
191
|
-
200: "rgba(245, 208, 254, 1)",
|
|
192
|
-
300: "rgba(240, 171, 252, 1)",
|
|
193
|
-
400: "rgba(232, 121, 249, 1)",
|
|
194
|
-
500: "rgba(217, 70, 239, 1)",
|
|
195
|
-
600: "rgba(192, 38, 211, 1)",
|
|
196
|
-
700: "rgba(162, 28, 175, 1)",
|
|
197
|
-
800: "rgba(134, 25, 143, 1)",
|
|
198
|
-
900: "rgba(112, 26, 117, 1)",
|
|
199
|
-
950: "rgba(74, 4, 78, 1)",
|
|
200
|
-
},
|
|
201
|
-
pink: {
|
|
202
|
-
50: "rgba(253, 242, 248, 1)",
|
|
203
|
-
100: "rgba(252, 231, 243, 1)",
|
|
204
|
-
200: "rgba(251, 207, 232, 1)",
|
|
205
|
-
300: "rgba(249, 168, 212, 1)",
|
|
206
|
-
400: "rgba(244, 114, 182, 1)",
|
|
207
|
-
500: "rgba(236, 72, 153, 1)",
|
|
208
|
-
600: "rgba(219, 39, 119, 1)",
|
|
209
|
-
700: "rgba(190, 24, 93, 1)",
|
|
210
|
-
800: "rgba(157, 23, 77, 1)",
|
|
211
|
-
900: "rgba(131, 24, 67, 1)",
|
|
212
|
-
950: "rgba(80, 7, 36, 1)",
|
|
213
|
-
},
|
|
214
|
-
rose: {
|
|
215
|
-
50: "rgba(255, 241, 242, 1)",
|
|
216
|
-
100: "rgba(255, 228, 230, 1)",
|
|
217
|
-
200: "rgba(254, 205, 211, 1)",
|
|
218
|
-
300: "rgba(253, 164, 175, 1)",
|
|
219
|
-
400: "rgba(251, 113, 133, 1)",
|
|
220
|
-
500: "rgba(244, 63, 94, 1)",
|
|
221
|
-
600: "rgba(225, 29, 72, 1)",
|
|
222
|
-
700: "rgba(190, 18, 60, 1)",
|
|
223
|
-
800: "rgba(159, 18, 57, 1)",
|
|
224
|
-
900: "rgba(136, 19, 55, 1)",
|
|
225
|
-
950: "rgba(76, 5, 25, 1)",
|
|
226
|
-
},
|
|
227
|
-
slate: {
|
|
228
|
-
50: "rgba(248, 250, 252, 1)",
|
|
229
|
-
100: "rgba(241, 245, 249, 1)",
|
|
230
|
-
200: "rgba(226, 232, 240, 1)",
|
|
231
|
-
300: "rgba(203, 213, 225, 1)",
|
|
232
|
-
400: "rgba(148, 163, 184, 1)",
|
|
233
|
-
500: "rgba(100, 116, 139, 1)",
|
|
234
|
-
600: "rgba(71, 85, 105, 1)",
|
|
235
|
-
700: "rgba(51, 65, 85, 1)",
|
|
236
|
-
800: "rgba(30, 41, 59, 1)",
|
|
237
|
-
900: "rgba(15, 23, 42, 1)",
|
|
238
|
-
950: "rgba(2, 6, 23, 1)",
|
|
239
|
-
},
|
|
240
|
-
gray: {
|
|
241
|
-
50: "rgba(249, 250, 251, 1)",
|
|
242
|
-
100: "rgba(243, 244, 246, 1)",
|
|
243
|
-
200: "rgba(229, 231, 235, 1)",
|
|
244
|
-
300: "rgba(209, 213, 219, 1)",
|
|
245
|
-
400: "rgba(156, 163, 175, 1)",
|
|
246
|
-
500: "rgba(107, 114, 128, 1)",
|
|
247
|
-
600: "rgba(75, 85, 99, 1)",
|
|
248
|
-
700: "rgba(55, 65, 81, 1)",
|
|
249
|
-
800: "rgba(31, 41, 55, 1)",
|
|
250
|
-
900: "rgba(17, 24, 39, 1)",
|
|
251
|
-
950: "rgba(3, 7, 18, 1)",
|
|
252
|
-
},
|
|
253
|
-
zinc: {
|
|
254
|
-
50: "rgba(250, 250, 250, 1)",
|
|
255
|
-
100: "rgba(244, 244, 245, 1)",
|
|
256
|
-
200: "rgba(228, 228, 231, 1)",
|
|
257
|
-
300: "rgba(212, 212, 216, 1)",
|
|
258
|
-
400: "rgba(161, 161, 170, 1)",
|
|
259
|
-
500: "rgba(113, 113, 122, 1)",
|
|
260
|
-
600: "rgba(82, 82, 91, 1)",
|
|
261
|
-
700: "rgba(63, 63, 70, 1)",
|
|
262
|
-
800: "rgba(39, 39, 42, 1)",
|
|
263
|
-
900: "rgba(24, 24, 27, 1)",
|
|
264
|
-
950: "rgba(9, 9, 11, 1)",
|
|
265
|
-
},
|
|
266
|
-
neutral: {
|
|
267
|
-
50: "rgba(250, 250, 250, 1)",
|
|
268
|
-
100: "rgba(245, 245, 245, 1)",
|
|
269
|
-
200: "rgba(229, 229, 229, 1)",
|
|
270
|
-
300: "rgba(212, 212, 212, 1)",
|
|
271
|
-
400: "rgba(163, 163, 163, 1)",
|
|
272
|
-
500: "rgba(115, 115, 115, 1)",
|
|
273
|
-
600: "rgba(82, 82, 82, 1)",
|
|
274
|
-
700: "rgba(64, 64, 64, 1)",
|
|
275
|
-
800: "rgba(38, 38, 38, 1)",
|
|
276
|
-
900: "rgba(23, 23, 23, 1)",
|
|
277
|
-
950: "rgba(10, 10, 10, 1)",
|
|
278
|
-
},
|
|
279
|
-
stone: {
|
|
280
|
-
50: "rgba(250, 250, 249, 1)",
|
|
281
|
-
100: "rgba(245, 245, 244, 1)",
|
|
282
|
-
200: "rgba(231, 229, 228, 1)",
|
|
283
|
-
300: "rgba(214, 211, 209, 1)",
|
|
284
|
-
400: "rgba(168, 162, 158, 1)",
|
|
285
|
-
500: "rgba(120, 113, 108, 1)",
|
|
286
|
-
600: "rgba(87, 83, 78, 1)",
|
|
287
|
-
700: "rgba(68, 64, 60, 1)",
|
|
288
|
-
800: "rgba(41, 37, 36, 1)",
|
|
289
|
-
900: "rgba(28, 25, 23, 1)",
|
|
290
|
-
950: "rgba(12, 10, 9, 1)",
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
export const colors = {
|
|
295
|
-
...palette,
|
|
296
|
-
border: palette.zinc["200"],
|
|
297
|
-
border_shadow: `0px 1px 2px 0px ${palette.zinc["200"]}`,
|
|
298
|
-
background: palette.white,
|
|
299
|
-
shadow: `0px 0px 6px 1px ${palette.zinc["300"]}`,
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* A low-alpha wash of a palette color (hover tints, dimmed chart segments,
|
|
304
|
-
* icon discs). Handles the palette's rgba() strings plus rgb()/#RRGGBB.
|
|
305
|
-
*/
|
|
306
|
-
export function withAlpha(color: string, alpha: number): string {
|
|
307
|
-
if (color.startsWith("rgba(")) return color.replace(/,\s*[\d.]+\s*\)\s*$/, `, ${alpha})`);
|
|
308
|
-
if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(/\)\s*$/, `, ${alpha})`);
|
|
309
|
-
if (color.startsWith("#") && color.length === 7) {
|
|
310
|
-
return `${color}${Math.round(alpha * 255)
|
|
311
|
-
.toString(16)
|
|
312
|
-
.padStart(2, "0")}`;
|
|
313
|
-
}
|
|
314
|
-
return color;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* A palette FAMILY name — the single, semantic way to reference a color across
|
|
319
|
-
* the system (Badge, status indicators, chart series, breakdown segments).
|
|
320
|
-
* Reference colors by NAME and let the component resolve the shade it needs;
|
|
321
|
-
* never thread a raw hex through props — it drifts (one call site picks 500,
|
|
322
|
-
* another 600, and the same "status" renders two greens). Excludes the
|
|
323
|
-
* non-scale role keys (border/background/shadow/black/white).
|
|
324
|
-
*/
|
|
325
|
-
export type ColorName = Exclude<keyof typeof colors, "border" | "border_shadow" | "background" | "shadow" | "black" | "white">;
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* The canonical SOLID shade of a family (500) — status dots, status-grid
|
|
329
|
-
* cells, breakdown segments, chart series. Defined ONCE so every indicator of
|
|
330
|
-
* the same color agrees on the shade.
|
|
331
|
-
*/
|
|
332
|
-
export function solid(name: ColorName): string {
|
|
333
|
-
return colors[name][500];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** A low-alpha wash of a family's solid shade — cell/segment tints, dimmed
|
|
337
|
-
* states. `tint("emerald", 0.2)` === `withAlpha(solid("emerald"), 0.2)`. */
|
|
338
|
-
export function tint(name: ColorName, alpha: number): string {
|
|
339
|
-
return withAlpha(solid(name), alpha);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** Dark → light stops a `ramp` spans (the usable mid range of a scale). */
|
|
343
|
-
const RAMP_STOPS = [700, 600, 500, 400, 300, 200] as const;
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* `count` distinct shades of ONE family, strong → light — the monochrome
|
|
347
|
-
* "one hue family per dimension" data ramp (a Breakdown's segments, an ordered
|
|
348
|
-
* funnel, any multi-category breakdown of a single dimension). Use this for a
|
|
349
|
-
* COHERENT dimension instead of hand-picking shades or scattering hues; the
|
|
350
|
-
* segment LABEL carries identity, the shade only orders. (For semantic
|
|
351
|
-
* categories whose color carries MEANING — status — give each its own
|
|
352
|
-
* `ColorName` and `solid()` it, don't ramp.)
|
|
353
|
-
*/
|
|
354
|
-
export function ramp(name: ColorName, count: number): string[] {
|
|
355
|
-
const scale = colors[name];
|
|
356
|
-
if (count <= 1) return [scale[600]];
|
|
357
|
-
return Array.from({ length: count }, (_, i) => {
|
|
358
|
-
const idx = Math.round((i / (count - 1)) * (RAMP_STOPS.length - 1));
|
|
359
|
-
return scale[RAMP_STOPS[idx]];
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Is `value` a usable {@link ColorName} — a palette FAMILY, not a role key
|
|
365
|
-
* (`border`/`background`/…) and not `black`/`white`? A family resolves to a
|
|
366
|
-
* shade object; the role/scalar keys resolve to a string, so "value is an
|
|
367
|
-
* object" is the test (no name list to keep in sync).
|
|
368
|
-
*/
|
|
369
|
-
export function isColorName(value: unknown): value is ColorName {
|
|
370
|
-
return (
|
|
371
|
-
typeof value === "string" &&
|
|
372
|
-
value in colors &&
|
|
373
|
-
typeof (colors as Record<string, unknown>)[value] === "object"
|
|
374
|
-
);
|
|
375
|
-
}
|
|
1
|
+
export * from "./color_tokens";
|
|
376
2
|
|
|
377
3
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
4
|
+
* The NATIVE token surface — the literal table, unchanged.
|
|
5
|
+
*
|
|
6
|
+
* The values themselves live in `./color_tokens`, which both this file and
|
|
7
|
+
* `./colors.web.ts` re-export. That indirection exists for one concrete reason:
|
|
8
|
+
* a relative `./colors` resolves `.web.ts` FIRST on web, so a web variant that
|
|
9
|
+
* imported `./colors` would import itself, and esbuild reports it as
|
|
10
|
+
* "Detected cycle while resolving import". Neither variant may name the other;
|
|
11
|
+
* both name a third module instead.
|
|
12
|
+
*
|
|
13
|
+
* Importers never see this split — `@lotics/ui/colors` and the kit's own
|
|
14
|
+
* `./colors` both keep working, and which of the two files answers is decided by
|
|
15
|
+
* platform (the `react-native` export condition, and `.web.ts` extension order
|
|
16
|
+
* inside the kit).
|
|
383
17
|
*/
|
|
384
|
-
export function asColorName(value: unknown, fallback: ColorName = "zinc"): ColorName {
|
|
385
|
-
return isColorName(value) ? value : fallback;
|
|
386
|
-
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { colors as contract } from "./color_tokens";
|
|
2
|
+
|
|
3
|
+
export { withAlpha, solid, tint, ramp, isColorName, asColorName } from "./color_tokens";
|
|
4
|
+
export type { ColorName } from "./color_tokens";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The token contract, with its FOUR themeable roles indirected through CSS
|
|
8
|
+
* variables so an app can carry its own visual identity.
|
|
9
|
+
*
|
|
10
|
+
* Why this file exists at all, rather than a React context: 136 of the kit's 320
|
|
11
|
+
* modules import `colors` directly, at module scope, into `StyleSheet.create`.
|
|
12
|
+
* A context would have to reach every one of them, turning static stylesheets
|
|
13
|
+
* into per-render inline styles — the kit's whole performance model — for a
|
|
14
|
+
* feature that changes four values. A variable costs nothing at runtime and
|
|
15
|
+
* touches no component: `colors.background` still reads `colors.background`.
|
|
16
|
+
*
|
|
17
|
+
* It is safe because react-native-web's `isWebColor` allowlists `var(` ahead of
|
|
18
|
+
* its own colour normalization, so a variable reaches CSS untouched on every
|
|
19
|
+
* colour property. That is a web-only guarantee, which is exactly why this is a
|
|
20
|
+
* `.web.ts` sibling: the `react-native` export condition and `.web.ts` extension
|
|
21
|
+
* order pick it for the browser; native keeps the literals it can parse.
|
|
22
|
+
*
|
|
23
|
+
* It reads its literals from `./color_tokens`, NEVER from `./colors` — on web a
|
|
24
|
+
* relative `./colors` resolves back to this very file, which esbuild reports as
|
|
25
|
+
* "Detected cycle while resolving import".
|
|
26
|
+
*
|
|
27
|
+
* Each variable carries its literal DEFAULT inline, so the theme is opt-in in
|
|
28
|
+
* the strictest sense — an app that sets nothing has no `--lotics-*` defined,
|
|
29
|
+
* every `var()` falls back, and the pixels are identical to before this file.
|
|
30
|
+
*
|
|
31
|
+
* ONLY these three. The palette families stay literal, and must: `withAlpha`,
|
|
32
|
+
* `tint` and `ramp` do string surgery on an `rgba()` to derive a wash, and a
|
|
33
|
+
* `var()` handed to them would produce garbage no type would catch. Keeping the
|
|
34
|
+
* themeable set to chrome keeps those functions total — the maintainability
|
|
35
|
+
* argument and the "colour must keep meaning status" argument turn out to be the
|
|
36
|
+
* same argument.
|
|
37
|
+
*/
|
|
38
|
+
export const colors = {
|
|
39
|
+
...contract,
|
|
40
|
+
background: `var(--lotics-background, ${contract.background})`,
|
|
41
|
+
border: `var(--lotics-border, ${contract.border})`,
|
|
42
|
+
accent: `var(--lotics-accent, ${contract.accent})`,
|
|
43
|
+
};
|
package/src/summary.tsx
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { Metric, type MetricFormat, type MetricTone } from "./metric";
|
|
4
|
+
import { StackedProgressBar } from "./stacked_progress_bar";
|
|
5
|
+
import { SummaryLine, type SummaryLineItem } from "./summary_line";
|
|
6
|
+
import { useLocaleTag } from "./locale";
|
|
7
|
+
import { Text } from "./text";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One bucket of the set being summarized — a slice of the whole, with the colour
|
|
11
|
+
* it wears wherever it appears.
|
|
12
|
+
*
|
|
13
|
+
* The buckets PARTITION the set: their values sum to the total, which is why the
|
|
14
|
+
* total is derived rather than passed. A count that belongs to the same rows but
|
|
15
|
+
* does not partition them — a condition rather than a position, "overdue" beside
|
|
16
|
+
* "draft / sent / paid" — is not a bucket and must not be one, or the bar starts
|
|
17
|
+
* claiming a share for something that is already inside another segment. Pass
|
|
18
|
+
* those to `Summary.Facts` as `extra`.
|
|
19
|
+
*/
|
|
20
|
+
export interface SummaryBucket {
|
|
21
|
+
key: string;
|
|
22
|
+
/** Reads AFTER the value, like every other label in this family. */
|
|
23
|
+
label: string;
|
|
24
|
+
value: number;
|
|
25
|
+
/** The segment's colour, and the swatch's. Use the same value this bucket
|
|
26
|
+
* wears in the rest of the product — a bar that disagrees with the badge
|
|
27
|
+
* beside it is worse than no bar. */
|
|
28
|
+
color: string;
|
|
29
|
+
/** What this bucket means — the ⓘ beside its count. */
|
|
30
|
+
info?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SummaryContext {
|
|
34
|
+
buckets: readonly SummaryBucket[];
|
|
35
|
+
total: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const Ctx = createContext<SummaryContext | null>(null);
|
|
39
|
+
|
|
40
|
+
function useSummary(part: string): SummaryContext {
|
|
41
|
+
const ctx = useContext(Ctx);
|
|
42
|
+
if (!ctx) throw new Error(`<Summary.${part}> must be rendered inside <Summary>.`);
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SummaryProps {
|
|
47
|
+
buckets: readonly SummaryBucket[];
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The register's summary band — a headline figure, the set's DISTRIBUTION, and
|
|
53
|
+
* the buckets named — composed from parts rather than configured by props.
|
|
54
|
+
*
|
|
55
|
+
* **Why a compound and not one component with more props.** The parts encode a
|
|
56
|
+
* correspondence that is easy to break and impossible to see broken: the bar,
|
|
57
|
+
* the legend and the headline all describe ONE set. Passing a `segments` array
|
|
58
|
+
* beside an `items` array beside a `total` invites exactly three drifts —
|
|
59
|
+
* segments reordered against their labels, a legend swatch picking a different
|
|
60
|
+
* shade, and a hardcoded total that stops summing after a bucket is added. So
|
|
61
|
+
* the buckets are declared ONCE on the root and every part derives from them.
|
|
62
|
+
* The total is a sum. The swatches are the segments' own colours. Nothing is
|
|
63
|
+
* restated, so nothing can disagree.
|
|
64
|
+
*
|
|
65
|
+
* That correspondence is the reason this is a primitive at all — this kit does
|
|
66
|
+
* not extract a composition for saving lines of layout.
|
|
67
|
+
*
|
|
68
|
+
* Reach for it when the set has a SHAPE worth showing. A summary that is only a
|
|
69
|
+
* few aggregates stays `SummaryLine`, which is this band's `Facts` row on its
|
|
70
|
+
* own; a boxed dashboard stat band is `KPIStrip`; a drill-down facet browser
|
|
71
|
+
* over a large population is `Breakdown`.
|
|
72
|
+
*
|
|
73
|
+
* <Summary buckets={STAGES}>
|
|
74
|
+
* <Summary.Header>
|
|
75
|
+
* <Summary.Total label="khách hàng" />
|
|
76
|
+
* <Summary.Metric value={660_000_000} label="hoa hồng" format="currency" compact />
|
|
77
|
+
* </Summary.Header>
|
|
78
|
+
* <Summary.Distribution />
|
|
79
|
+
* <Summary.Facts extra={[{ label: "đang tắc", value: 3, tone: "warning" }]} />
|
|
80
|
+
* </Summary>
|
|
81
|
+
*/
|
|
82
|
+
export function Summary(props: SummaryProps) {
|
|
83
|
+
const { buckets, children } = props;
|
|
84
|
+
const total = buckets.reduce((sum, b) => sum + b.value, 0);
|
|
85
|
+
return (
|
|
86
|
+
<Ctx.Provider value={{ buckets, total }}>
|
|
87
|
+
<View style={styles.root}>{children}</View>
|
|
88
|
+
</Ctx.Provider>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The band's top line: a `Total` at the start, anything else pushed to the end.
|
|
93
|
+
* Baseline-aligned, because figures of different sizes sitting on one line is
|
|
94
|
+
* the whole reason this row exists. */
|
|
95
|
+
function Header(props: { children: ReactNode }) {
|
|
96
|
+
return <View style={styles.header}>{props.children}</View>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The set's SIZE, at display scale — the band's headline.
|
|
101
|
+
*
|
|
102
|
+
* The number is the sum of the buckets and is never passed in. A page that
|
|
103
|
+
* restates its own total eventually restates it wrongly: a bucket gets added,
|
|
104
|
+
* the constant does not move, and the headline quietly disagrees with the bar
|
|
105
|
+
* directly beneath it.
|
|
106
|
+
*/
|
|
107
|
+
function Total(props: { label: string; formatValue?: (n: number) => string }) {
|
|
108
|
+
const { total } = useSummary("Total");
|
|
109
|
+
const tag = useLocaleTag();
|
|
110
|
+
// Grouped by the reader's locale by default — a set of 12,345 was rendering
|
|
111
|
+
// "12345" in the one place on the screen sized to be read first, because the
|
|
112
|
+
// first app to use this counted in the hundreds. `formatValue` is the escape
|
|
113
|
+
// hatch for a total whose unit is not a plain count.
|
|
114
|
+
const shown = props.formatValue ? props.formatValue(total) : total.toLocaleString(tag);
|
|
115
|
+
return (
|
|
116
|
+
<View style={styles.pair}>
|
|
117
|
+
<Text size="xxxl" weight="semibold" tabular>
|
|
118
|
+
{shown}
|
|
119
|
+
</Text>
|
|
120
|
+
<Text size="md" color="muted">
|
|
121
|
+
{props.label}
|
|
122
|
+
</Text>
|
|
123
|
+
</View>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A second headline figure that is NOT part of the distribution — a sum of money
|
|
129
|
+
* over the same rows, an average, a rate.
|
|
130
|
+
*
|
|
131
|
+
* Deliberately not a bucket: it does not partition the set, so the bar must not
|
|
132
|
+
* carry it. It sits on the header row because a reader opening a register asks
|
|
133
|
+
* two questions ("how much of this is there" and "what is it worth"), and the
|
|
134
|
+
* answers belong side by side.
|
|
135
|
+
*/
|
|
136
|
+
function SummaryMetric(props: {
|
|
137
|
+
value: number | string | null | undefined;
|
|
138
|
+
label: string;
|
|
139
|
+
format?: MetricFormat;
|
|
140
|
+
currency?: string;
|
|
141
|
+
compact?: boolean;
|
|
142
|
+
emptyLabel?: string;
|
|
143
|
+
tone?: MetricTone;
|
|
144
|
+
}) {
|
|
145
|
+
const { value, label, ...rest } = props;
|
|
146
|
+
return (
|
|
147
|
+
<View style={styles.pair}>
|
|
148
|
+
<Metric value={value} size="lg" {...rest} />
|
|
149
|
+
<Text size="sm" color="muted">
|
|
150
|
+
{label}
|
|
151
|
+
</Text>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The set's shape — one segment per bucket, in declaration order.
|
|
158
|
+
*
|
|
159
|
+
* Order is the CALLER's, untouched, because for an ordered set (a pipeline, a
|
|
160
|
+
* ladder, an age bracket) the sequence carries meaning that sorting by size
|
|
161
|
+
* would destroy. Re-rank the array before passing it if size is what matters.
|
|
162
|
+
*/
|
|
163
|
+
function Distribution(props: { height?: number }) {
|
|
164
|
+
const { buckets, total } = useSummary("Distribution");
|
|
165
|
+
return (
|
|
166
|
+
<StackedProgressBar
|
|
167
|
+
height={props.height ?? 20}
|
|
168
|
+
total={total}
|
|
169
|
+
segments={buckets.map((b) => ({ key: b.key, value: b.value, color: b.color }))}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The buckets named, with their counts — the band's legend, and on its own the
|
|
176
|
+
* whole of `SummaryLine`.
|
|
177
|
+
*
|
|
178
|
+
* Each item takes its swatch from its own bucket, so the legend cannot drift
|
|
179
|
+
* from the bar above it. `extra` appends aggregates that are NOT segments and
|
|
180
|
+
* therefore get no swatch — the visual difference is the point: a reader can see
|
|
181
|
+
* that the unswatched figure is not a slice of the bar.
|
|
182
|
+
*/
|
|
183
|
+
function Facts(props: { extra?: readonly SummaryLineItem[] }) {
|
|
184
|
+
const { buckets } = useSummary("Facts");
|
|
185
|
+
const items: SummaryLineItem[] = buckets.map((b) => ({
|
|
186
|
+
label: b.label,
|
|
187
|
+
value: b.value,
|
|
188
|
+
color: b.color,
|
|
189
|
+
info: b.info,
|
|
190
|
+
}));
|
|
191
|
+
return <SummaryLine items={[...items, ...(props.extra ?? [])]} />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Summary.Header = Header;
|
|
195
|
+
Summary.Total = Total;
|
|
196
|
+
Summary.Metric = SummaryMetric;
|
|
197
|
+
Summary.Distribution = Distribution;
|
|
198
|
+
Summary.Facts = Facts;
|
|
199
|
+
|
|
200
|
+
const styles = StyleSheet.create({
|
|
201
|
+
// 12, and it is half of what a page must put BETWEEN this band and its
|
|
202
|
+
// neighbours. The band's parts are one group — a figure, the shape of what it
|
|
203
|
+
// counts, and that shape's key — so they have to sit closer to each other than
|
|
204
|
+
// the band sits to the register below it. Measured at 14 against a 16px page
|
|
205
|
+
// gap, the ratio was 1.14: every block on the screen an equal peer, nothing
|
|
206
|
+
// grouped, which is what "no rhythm" is when you measure it.
|
|
207
|
+
root: { gap: 12 },
|
|
208
|
+
header: {
|
|
209
|
+
flexDirection: "row",
|
|
210
|
+
alignItems: "baseline",
|
|
211
|
+
flexWrap: "wrap",
|
|
212
|
+
justifyContent: "space-between",
|
|
213
|
+
columnGap: 20,
|
|
214
|
+
rowGap: 8,
|
|
215
|
+
},
|
|
216
|
+
pair: { flexDirection: "row", alignItems: "baseline", gap: 8 },
|
|
217
|
+
});
|