@lotics/ui 42.0.0 → 42.4.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 +41 -0
- package/docs/catalog.md +26 -1
- 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/locale.tsx +7 -0
- package/src/picker.tsx +50 -5
- package/src/picker_empty_option.ts +30 -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/locale.tsx
CHANGED
|
@@ -47,6 +47,11 @@ export interface LoticsLocale {
|
|
|
47
47
|
* in-cell editors): the select-all/deselect-all links, the empty state, the
|
|
48
48
|
* internal search-field placeholder, and the `Combobox` recents header. */
|
|
49
49
|
optionList: { selectAll: string; deselectAll: string; clear: string; noResults: string; recent: string; searchPlaceholder: string };
|
|
50
|
+
/** `Picker`'s empty option, when the caller declares empty a CHOICE
|
|
51
|
+
* (`includeEmptyOption`). A value, not an action — hence "None" rather than
|
|
52
|
+
* the option list's "Clear": in a native `<select>` the reader picks it the
|
|
53
|
+
* same way they pick any other row, so it has to read like one. */
|
|
54
|
+
picker: { emptyOption: string };
|
|
50
55
|
datePicker: DatePickerLabels;
|
|
51
56
|
calendar: CalendarLabels;
|
|
52
57
|
/** `FilterChip` (and `ColumnFilter`): the generic clear affordance, used when
|
|
@@ -266,6 +271,7 @@ export const en: LoticsLocale = {
|
|
|
266
271
|
},
|
|
267
272
|
referenceField: { open: "Open", change: "Change", clear: "Clear", edit: "Edit", save: "Save", saving: "Saving…", cancel: "Cancel" },
|
|
268
273
|
optionList: { selectAll: "Select all", deselectAll: "Deselect all", clear: "Clear", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…" },
|
|
274
|
+
picker: { emptyOption: "None" },
|
|
269
275
|
datePicker: { today: "Today", now: "Now", clear: "Clear", done: "Done", openCalendar: "Open calendar", time: "Time", startTime: "Start time", endTime: "End time", startDate: "Start date", endDate: "End date", addTime: "Add time", removeTime: "Remove time", year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM", invalidDate: "Enter a complete date", invalidTime: "Enter a complete time" },
|
|
270
276
|
calendar: { previousMonth: "Previous month", nextMonth: "Next month" },
|
|
271
277
|
filterChip: { clear: "Clear" },
|
|
@@ -437,6 +443,7 @@ export const vi: LoticsLocale = {
|
|
|
437
443
|
},
|
|
438
444
|
referenceField: { open: "Mở", change: "Đổi", clear: "Bỏ chọn", edit: "Sửa", save: "Lưu", saving: "Đang lưu…", cancel: "Huỷ" },
|
|
439
445
|
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…" },
|
|
446
|
+
picker: { emptyOption: "Không có" },
|
|
440
447
|
datePicker: { today: "Hôm nay", now: "Bây giờ", clear: "Xóa", done: "Xong", openCalendar: "Mở lịch", time: "Giờ", startTime: "Giờ bắt đầu", endTime: "Giờ kết thúc", startDate: "Ngày bắt đầu", endDate: "Ngày kết thúc", addTime: "Thêm giờ", removeTime: "Bỏ giờ", year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH", invalidDate: "Nhập ngày đầy đủ", invalidTime: "Nhập giờ đầy đủ" },
|
|
441
448
|
calendar: { previousMonth: "Tháng trước", nextMonth: "Tháng sau" },
|
|
442
449
|
filterChip: { clear: "Xóa" },
|
package/src/picker.tsx
CHANGED
|
@@ -7,6 +7,9 @@ import { useFocusRing } from "./use_focus_ring";
|
|
|
7
7
|
import { useHover } from "./use_hover";
|
|
8
8
|
import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
|
|
9
9
|
import { Icon } from "./icon";
|
|
10
|
+
import { useLoticsLocale } from "./locale";
|
|
11
|
+
import { useFormField } from "./form_field";
|
|
12
|
+
import { pickerEmptyOptionLabel } from "./picker_empty_option";
|
|
10
13
|
|
|
11
14
|
export interface PickerOption<T extends string = string, D = unknown> {
|
|
12
15
|
label?: string;
|
|
@@ -41,7 +44,15 @@ export interface PickerProps<T extends string = string> {
|
|
|
41
44
|
testID?: string;
|
|
42
45
|
disabled?: boolean;
|
|
43
46
|
autoFocus?: boolean;
|
|
47
|
+
/** Declares empty a CHOICE: the reader may pick it to clear the field, so the
|
|
48
|
+
* option stays in the list after something is selected. Because it is a value
|
|
49
|
+
* they pick, it is NAMED — see `emptyOptionLabel`. */
|
|
44
50
|
includeEmptyOption?: boolean;
|
|
51
|
+
/** Overrides the name of that empty choice (locale default: "None"). Distinct
|
|
52
|
+
* from `placeholder`, which is the hint shown while nothing is chosen: a hint
|
|
53
|
+
* and a value are different jobs, and one string cannot do both — read as a
|
|
54
|
+
* prompt it invites input, read as a row it looks selectable. */
|
|
55
|
+
emptyOptionLabel?: string;
|
|
45
56
|
value?: T | null;
|
|
46
57
|
onValueChange?: (value: T) => void;
|
|
47
58
|
}
|
|
@@ -61,12 +72,25 @@ export function Picker<T extends string>(props: PickerProps<T>) {
|
|
|
61
72
|
includeEmptyOption,
|
|
62
73
|
onValueChange,
|
|
63
74
|
placeholder,
|
|
75
|
+
emptyOptionLabel,
|
|
64
76
|
accessibilityLabel,
|
|
65
77
|
style,
|
|
66
78
|
disabled = false,
|
|
67
79
|
autoFocus = false,
|
|
68
80
|
} = props;
|
|
69
81
|
|
|
82
|
+
const binding = useFormField();
|
|
83
|
+
const describedBy =
|
|
84
|
+
[binding?.descriptionId, binding?.warningId, binding?.errorId].filter(Boolean).join(" ") ||
|
|
85
|
+
undefined;
|
|
86
|
+
const locale = useLoticsLocale();
|
|
87
|
+
const emptyLabel = pickerEmptyOptionLabel({
|
|
88
|
+
includeEmptyOption,
|
|
89
|
+
emptyOptionLabel,
|
|
90
|
+
placeholder,
|
|
91
|
+
localeEmptyOption: locale.picker.emptyOption,
|
|
92
|
+
});
|
|
93
|
+
|
|
70
94
|
const pickerRef = useRef<RNPicker<string>>(null);
|
|
71
95
|
// The native <select> is a text-like control — ring on any focus (like the inputs).
|
|
72
96
|
const { focusVisible, focusProps } = useFocusRing({ always: true });
|
|
@@ -100,7 +124,18 @@ export function Picker<T extends string>(props: PickerProps<T>) {
|
|
|
100
124
|
<RNPicker
|
|
101
125
|
ref={pickerRef}
|
|
102
126
|
testID={testID}
|
|
103
|
-
|
|
127
|
+
// Inside a `FormField` the VISIBLE label names the control, the same way
|
|
128
|
+
// it does for `NumberInput` / `TextInputField`. Without this a
|
|
129
|
+
// `FormPicker` rendered a label the select was never associated with, so
|
|
130
|
+
// it announced its current value and no name at all — the label was
|
|
131
|
+
// there for sighted readers only. An explicit `accessibilityLabel` is
|
|
132
|
+
// used only OUTSIDE a field: inside one, an aria-label that differs from
|
|
133
|
+
// the visible text is what breaks label-in-name.
|
|
134
|
+
id={binding?.inputId}
|
|
135
|
+
aria-labelledby={binding?.labelId}
|
|
136
|
+
aria-label={!binding ? accessibilityLabel : undefined}
|
|
137
|
+
aria-describedby={describedBy}
|
|
138
|
+
aria-invalid={binding?.invalid || undefined}
|
|
104
139
|
onFocus={focusProps.onFocus}
|
|
105
140
|
onBlur={focusProps.onBlur}
|
|
106
141
|
// Empty selection maps to "" (the placeholder option's value), never
|
|
@@ -113,10 +148,20 @@ export function Picker<T extends string>(props: PickerProps<T>) {
|
|
|
113
148
|
enabled={!disabled}
|
|
114
149
|
>
|
|
115
150
|
{(!value || includeEmptyOption) && (
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
151
|
+
// Two different jobs share this one row, and which one it is doing
|
|
152
|
+
// depends on `includeEmptyOption`:
|
|
153
|
+
//
|
|
154
|
+
// A CHOICE (`includeEmptyOption`) — the reader picks it to clear the
|
|
155
|
+
// field, so it is named, and named the SAME whether or not something
|
|
156
|
+
// is currently selected. It previously went blank the moment a value
|
|
157
|
+
// existed, which is exactly when the reader needs it: every select in
|
|
158
|
+
// the product offered an unlabelled row as its only way back to empty.
|
|
159
|
+
//
|
|
160
|
+
// A PLACEHOLDER (otherwise) — it exists only because nothing is
|
|
161
|
+
// chosen yet, so it carries the caller's hint and disappears on
|
|
162
|
+
// selection. Naming it "None" here would turn a prompt into an
|
|
163
|
+
// apparent value.
|
|
164
|
+
<RNPicker.Item label={emptyLabel} value="" />
|
|
120
165
|
)}
|
|
121
166
|
{options.map((option) =>
|
|
122
167
|
option ? (
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `Picker`'s empty row is called — and it depends on which of two jobs that
|
|
3
|
+
* row is doing.
|
|
4
|
+
*
|
|
5
|
+
* A CHOICE (`includeEmptyOption`) is how the reader gets back to no value. They
|
|
6
|
+
* pick it the way they pick any other row, so it is NAMED, and named the same
|
|
7
|
+
* whether or not something is currently selected — a name that vanishes on
|
|
8
|
+
* selection vanishes exactly when it is needed.
|
|
9
|
+
*
|
|
10
|
+
* A PLACEHOLDER exists only because nothing is chosen yet. It carries the
|
|
11
|
+
* caller's hint and leaves on selection; naming it "None" would turn a prompt
|
|
12
|
+
* into an apparent value.
|
|
13
|
+
*
|
|
14
|
+
* RN-free so the rule is testable as the rule, rather than through a native
|
|
15
|
+
* `<select>` and its untranspiled dependency.
|
|
16
|
+
*/
|
|
17
|
+
export function pickerEmptyOptionLabel(opts: {
|
|
18
|
+
includeEmptyOption?: boolean;
|
|
19
|
+
/** Per-instance override for the choice's name. */
|
|
20
|
+
emptyOptionLabel?: string;
|
|
21
|
+
/** The caller's hint, used only when the row is a placeholder. */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** The locale pack's word for "no value". */
|
|
24
|
+
localeEmptyOption: string;
|
|
25
|
+
}): string {
|
|
26
|
+
if (opts.includeEmptyOption) {
|
|
27
|
+
return opts.emptyOptionLabel ?? opts.localeEmptyOption;
|
|
28
|
+
}
|
|
29
|
+
return opts.placeholder ?? "";
|
|
30
|
+
}
|