@lotics/ui 41.4.1 → 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/MIGRATION.md +34 -0
- package/docs/catalog.md +21 -0
- package/docs/composition.md +23 -2
- package/examples/tpl_dashboard.tsx +1 -1
- 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/date_filter.tsx +54 -34
- package/src/date_filter_presets.ts +15 -10
- package/src/locale.tsx +2 -2
- 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/date_filter.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { CONTROL_RADIUS } from "./control_surface";
|
|
|
6
6
|
import { MenuButton } from "./menu_button";
|
|
7
7
|
import { Calendar, CalendarRangeValue, CalendarRef } from "./date_calendar";
|
|
8
8
|
import { TimePicker } from "./time_picker";
|
|
9
|
-
import {
|
|
9
|
+
import { useScreenSize } from "./use_screen_size";
|
|
10
10
|
import { SegmentLabels } from "./date_segments";
|
|
11
11
|
import { PresetId, PRESET_IDS, getPresetValue } from "./date_filter_presets";
|
|
12
12
|
import { formatDate } from "./format_date";
|
|
@@ -33,7 +33,6 @@ export interface DateFilterLabels extends SegmentLabels {
|
|
|
33
33
|
thisWeek: string;
|
|
34
34
|
thisMonth: string;
|
|
35
35
|
lastMonth: string;
|
|
36
|
-
custom: string;
|
|
37
36
|
from: string;
|
|
38
37
|
to: string;
|
|
39
38
|
/** Accessible name of the field that OPENS this panel (`DateRangeFilterField`). */
|
|
@@ -72,8 +71,6 @@ function presetLabel(id: PresetId, labels: DateFilterLabels): string {
|
|
|
72
71
|
return labels.thisMonth;
|
|
73
72
|
case "last_month":
|
|
74
73
|
return labels.lastMonth;
|
|
75
|
-
case "custom":
|
|
76
|
-
return labels.custom;
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -134,7 +131,21 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
134
131
|
() => ({ ...loc, ...props.labels }),
|
|
135
132
|
[loc, props.labels],
|
|
136
133
|
);
|
|
137
|
-
|
|
134
|
+
// The VIEWPORT, never this panel's own box.
|
|
135
|
+
//
|
|
136
|
+
// This read `useContainerSize()`, which inside a `Popover` measures the popover
|
|
137
|
+
// BODY — and the body's width is decided by which branch this very line picks.
|
|
138
|
+
// Two calendars come to ~617px, always under the 768 phone threshold, so the
|
|
139
|
+
// panel resolved "small" on a 1440px desktop and the sidebar branch below was
|
|
140
|
+
// unreachable: the presets rendered as the phone strip on every screen, which
|
|
141
|
+
// is why the only item in them with any chrome read as a button.
|
|
142
|
+
//
|
|
143
|
+
// `Popover` already carries this rule for its own sheet-vs-anchored decision,
|
|
144
|
+
// and its comment names the same bug being introduced by the same hook. Same
|
|
145
|
+
// instrument here, so the two agree: under 768 the popover is a bottom sheet
|
|
146
|
+
// and the presets are a strip; at or above it, an anchored panel with the
|
|
147
|
+
// sidebar.
|
|
148
|
+
const screenSize = useScreenSize();
|
|
138
149
|
const calendarRef = useRef<CalendarRef>(null);
|
|
139
150
|
|
|
140
151
|
// A half-picked range is held HERE and never emitted — the same rule
|
|
@@ -192,13 +203,15 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
192
203
|
[onValueChange, value],
|
|
193
204
|
);
|
|
194
205
|
|
|
195
|
-
// Which preset (if any) the current value matches.
|
|
206
|
+
// Which preset (if any) the current value matches. A hand-picked range matches
|
|
207
|
+
// none, and that is `null` — no row is selected. It used to fall through to
|
|
208
|
+
// "custom", so the panel opened with a row lit for a range the member had
|
|
209
|
+
// never chosen from this list.
|
|
196
210
|
const activePresetId = useMemo((): PresetId | null => {
|
|
197
211
|
if (!value.start.date && !value.end.date) return null;
|
|
198
212
|
const now = new Date();
|
|
199
213
|
for (const id of PRESET_IDS) {
|
|
200
214
|
const presetValue = getPresetValue(id, now);
|
|
201
|
-
if (!presetValue) continue; // "custom"
|
|
202
215
|
if (
|
|
203
216
|
value.start.date?.toDateString() === presetValue.start.date?.toDateString() &&
|
|
204
217
|
value.end.date?.toDateString() === presetValue.end.date?.toDateString()
|
|
@@ -206,7 +219,7 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
206
219
|
return id;
|
|
207
220
|
}
|
|
208
221
|
}
|
|
209
|
-
return
|
|
222
|
+
return null;
|
|
210
223
|
}, [value]);
|
|
211
224
|
|
|
212
225
|
const handlePresetSelect = useCallback(
|
|
@@ -220,12 +233,6 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
const presetValue = getPresetValue(id, new Date());
|
|
223
|
-
if (!presetValue) {
|
|
224
|
-
// "Custom" clears the value so the user can select manually
|
|
225
|
-
onValueChange({ start: { date: null, time: null }, end: { date: null, time: null } });
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
236
|
onValueChange(presetValue);
|
|
230
237
|
|
|
231
238
|
if (presetValue.start.date) {
|
|
@@ -238,27 +245,39 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
238
245
|
[onValueChange, activePresetId],
|
|
239
246
|
);
|
|
240
247
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
248
|
+
// `selected` + `role="option"` is the kit's listbox row, and `MenuButton` owns
|
|
249
|
+
// both halves of it: the resting `zinc.100` highlight and the `aria-selected`
|
|
250
|
+
// that carries the state to a screen reader.
|
|
251
|
+
//
|
|
252
|
+
// This used to paint `zinc.800` with inverted text and pass neither. That is
|
|
253
|
+
// the PRIMARY BUTTON treatment — the loudest thing the system has, spent on a
|
|
254
|
+
// row that reports state rather than offering an action, so the selected
|
|
255
|
+
// preset outshouted the panel's own Done button. And because no `selected` or
|
|
256
|
+
// `current` ever reached `MenuButton`, the active preset announced nothing at
|
|
257
|
+
// all: the state was visible only to someone who could see the fill.
|
|
258
|
+
const renderPreset = (id: PresetId) => (
|
|
259
|
+
<MenuButton
|
|
260
|
+
key={id}
|
|
261
|
+
role="option"
|
|
262
|
+
selected={activePresetId === id}
|
|
263
|
+
title={<Text size="sm">{presetLabel(id, labels)}</Text>}
|
|
264
|
+
onPress={() => handlePresetSelect(id)}
|
|
265
|
+
style={styles.presetItem}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
256
268
|
|
|
257
269
|
return (
|
|
258
270
|
<View style={[styles.container, { flexDirection: screenSize.small ? "column" : "row" }]}>
|
|
259
271
|
{/* Presets sidebar (desktop only) */}
|
|
260
272
|
{!screenSize.small && (
|
|
261
|
-
<View
|
|
273
|
+
<View
|
|
274
|
+
style={styles.presetsSidebar}
|
|
275
|
+
accessibilityLabel={labels.selectDateRange}
|
|
276
|
+
// `listbox` is valid ARIA but absent from RN's Role enum; rn-web forwards it.
|
|
277
|
+
role={"listbox" as "list"}
|
|
278
|
+
>
|
|
279
|
+
{PRESET_IDS.map(renderPreset)}
|
|
280
|
+
</View>
|
|
262
281
|
)}
|
|
263
282
|
|
|
264
283
|
{/* Calendar and time pickers */}
|
|
@@ -322,7 +341,11 @@ const styles = StyleSheet.create({
|
|
|
322
341
|
gap: 16,
|
|
323
342
|
},
|
|
324
343
|
presetsSidebar: {
|
|
325
|
-
|
|
344
|
+
// 128, not 140: the calendars come to ~617px and a popover is only anchored
|
|
345
|
+
// (rather than a bottom sheet) at viewports of 768 and up, so the sidebar
|
|
346
|
+
// has ~135px to spend before the panel is wider than the narrowest screen
|
|
347
|
+
// that shows it. The longest preset label sits well inside 128.
|
|
348
|
+
minWidth: 128,
|
|
326
349
|
},
|
|
327
350
|
presetsScrollView: {
|
|
328
351
|
borderTopWidth: 1,
|
|
@@ -337,9 +360,6 @@ const styles = StyleSheet.create({
|
|
|
337
360
|
marginHorizontal: 0,
|
|
338
361
|
marginBottom: 2,
|
|
339
362
|
},
|
|
340
|
-
presetItemActive: {
|
|
341
|
-
backgroundColor: colors.zinc["800"],
|
|
342
|
-
},
|
|
343
363
|
mainContent: {
|
|
344
364
|
flex: 1,
|
|
345
365
|
},
|
|
@@ -5,7 +5,6 @@ import type { DateFilterValue } from "./date_filter";
|
|
|
5
5
|
// `now`, never an ambient clock.
|
|
6
6
|
|
|
7
7
|
export type PresetId =
|
|
8
|
-
| "custom"
|
|
9
8
|
| "today"
|
|
10
9
|
| "yesterday"
|
|
11
10
|
| "tomorrow"
|
|
@@ -13,7 +12,17 @@ export type PresetId =
|
|
|
13
12
|
| "this_month"
|
|
14
13
|
| "last_month";
|
|
15
14
|
|
|
16
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Display order. Every id here SETS a range — that is what makes the list a set
|
|
17
|
+
* of presets rather than a set of modes.
|
|
18
|
+
*
|
|
19
|
+
* "custom" used to sit at the end and it was the one entry that set nothing: it
|
|
20
|
+
* CLEARED the value. So it lit up whenever the range matched no preset — which
|
|
21
|
+
* is the normal state for any hand-picked range — and clicking the lit item,
|
|
22
|
+
* the obvious move for "let me pick my own dates", threw the range away. The
|
|
23
|
+
* calendar above it is the custom picker, and the panel's Clear button is the
|
|
24
|
+
* clear; the item was a third name for two controls that were already there.
|
|
25
|
+
*/
|
|
17
26
|
export const PRESET_IDS: PresetId[] = [
|
|
18
27
|
"today",
|
|
19
28
|
"yesterday",
|
|
@@ -21,7 +30,6 @@ export const PRESET_IDS: PresetId[] = [
|
|
|
21
30
|
"this_week",
|
|
22
31
|
"this_month",
|
|
23
32
|
"last_month",
|
|
24
|
-
"custom",
|
|
25
33
|
];
|
|
26
34
|
|
|
27
35
|
function startOfDay(date: Date): Date {
|
|
@@ -66,12 +74,11 @@ function range(start: Date, end: Date): DateFilterValue {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
|
-
* Resolve a preset to a concrete date range relative to `now`.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* resolved range round-trips back to the same preset.
|
|
77
|
+
* Resolve a preset to a concrete date range relative to `now`. Total — every
|
|
78
|
+
* `PresetId` names a range. The boundary math is identical to the view-page
|
|
79
|
+
* filter so a resolved range round-trips back to the same preset.
|
|
73
80
|
*/
|
|
74
|
-
export function getPresetValue(id: PresetId, now: Date): DateFilterValue
|
|
81
|
+
export function getPresetValue(id: PresetId, now: Date): DateFilterValue {
|
|
75
82
|
switch (id) {
|
|
76
83
|
case "today":
|
|
77
84
|
return range(startOfDay(now), endOfDay(now));
|
|
@@ -94,7 +101,5 @@ export function getPresetValue(id: PresetId, now: Date): DateFilterValue | null
|
|
|
94
101
|
d.setMonth(d.getMonth() - 1);
|
|
95
102
|
return range(startOfMonth(d), endOfMonth(d));
|
|
96
103
|
}
|
|
97
|
-
case "custom":
|
|
98
|
-
return null;
|
|
99
104
|
}
|
|
100
105
|
}
|