react-native-terra-ui 0.11.0 → 0.11.2
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/CHANGELOG.md +12 -0
- package/lib/module/components/badge/Badge.js +92 -0
- package/lib/module/components/badge/Badge.js.map +1 -0
- package/lib/module/components/badge/index.js +4 -0
- package/lib/module/components/badge/index.js.map +1 -0
- package/lib/module/components/badge/parts/BadgeIndicator.js +212 -0
- package/lib/module/components/badge/parts/BadgeIndicator.js.map +1 -0
- package/lib/module/components/badge/types.js +4 -0
- package/lib/module/components/badge/types.js.map +1 -0
- package/lib/module/components/badge/utils.js +83 -0
- package/lib/module/components/badge/utils.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/src/components/badge/Badge.d.ts +17 -0
- package/lib/typescript/src/components/badge/Badge.d.ts.map +1 -0
- package/lib/typescript/src/components/badge/index.d.ts +3 -0
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -0
- package/lib/typescript/src/components/badge/parts/BadgeIndicator.d.ts +19 -0
- package/lib/typescript/src/components/badge/parts/BadgeIndicator.d.ts.map +1 -0
- package/lib/typescript/src/components/badge/types.d.ts +61 -0
- package/lib/typescript/src/components/badge/types.d.ts.map +1 -0
- package/lib/typescript/src/components/badge/utils.d.ts +40 -0
- package/lib/typescript/src/components/badge/utils.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/badge/Badge.tsx +78 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge/parts/BadgeIndicator.tsx +178 -0
- package/src/components/badge/types.ts +68 -0
- package/src/components/badge/utils.ts +77 -0
- package/src/components/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.2](https://github.com/earthlin9/react-native-terra-ui/compare/v0.11.1...v0.11.2) (2026-09-12)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **ui:** [badge] add offset prop and enlarge the indicator ([da293fa](https://github.com/earthlin9/react-native-terra-ui/commit/da293fa887d81cfeaf45f19067734e05ac4c6320))
|
|
8
|
+
|
|
9
|
+
## [0.11.1](https://github.com/earthlin9/react-native-terra-ui/compare/v0.11.0...v0.11.1) (2026-09-12)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **ui:** [badge] add Badge wrapper component ([4c3f49c](https://github.com/earthlin9/react-native-terra-ui/commit/4c3f49c9725f77bf4cbd479094335d710190d978))
|
|
14
|
+
|
|
3
15
|
## [0.11.0](https://github.com/earthlin9/react-native-terra-ui/compare/v0.10.2...v0.11.0) (2026-09-12)
|
|
4
16
|
|
|
5
17
|
### ⚠ BREAKING CHANGES
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
6
|
+
import { BadgeIndicator } from "./parts/BadgeIndicator.js";
|
|
7
|
+
import { getAnchorPosition } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Overlays a count or status indicator on one corner of the element it wraps.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Badge placement="top-right" color="danger" label={3}>
|
|
14
|
+
* <Toolbar.Button icon="bell" accessibilityLabel="Notifications" />
|
|
15
|
+
* </Badge>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The child renders untouched — `Badge` contributes the positioning context and
|
|
19
|
+
* the overlay, nothing else. A round child usually wants a negative `offset`,
|
|
20
|
+
* since the corner being anchored to is the bounding box's, not the circle's.
|
|
21
|
+
*/
|
|
22
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
23
|
+
const Badge = /*#__PURE__*/forwardRef(function Badge({
|
|
24
|
+
children,
|
|
25
|
+
placement = "top-right",
|
|
26
|
+
label,
|
|
27
|
+
color = "danger",
|
|
28
|
+
size = "md",
|
|
29
|
+
offset = 0,
|
|
30
|
+
isVisible = true,
|
|
31
|
+
fullWidth = false,
|
|
32
|
+
accessibilityLabel,
|
|
33
|
+
style,
|
|
34
|
+
...rest
|
|
35
|
+
}, ref) {
|
|
36
|
+
const _styles = styles;
|
|
37
|
+
{
|
|
38
|
+
const styles = _styles.useVariants({
|
|
39
|
+
fullWidth
|
|
40
|
+
});
|
|
41
|
+
return (
|
|
42
|
+
/*#__PURE__*/
|
|
43
|
+
// `style` last so an explicit `alignSelf` beats `fullWidth`.
|
|
44
|
+
_jsxs(View, {
|
|
45
|
+
ref: ref,
|
|
46
|
+
style: [styles.wrapper, style],
|
|
47
|
+
...rest,
|
|
48
|
+
children: [children, isVisible ? /*#__PURE__*/_jsx(View, {
|
|
49
|
+
pointerEvents: "box-none",
|
|
50
|
+
style: [styles.anchor, getAnchorPosition(placement, size, offset)],
|
|
51
|
+
children: /*#__PURE__*/_jsx(BadgeIndicator, {
|
|
52
|
+
label: label,
|
|
53
|
+
color: color,
|
|
54
|
+
size: size,
|
|
55
|
+
accessibilityLabel: accessibilityLabel
|
|
56
|
+
})
|
|
57
|
+
}) : null]
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
export { Badge };
|
|
63
|
+
|
|
64
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
65
|
+
//
|
|
66
|
+
// The indicator's insets are computed in `getAnchorPosition`, not declared here:
|
|
67
|
+
// `offset` is an open-ended number, so it could never be a variant condition,
|
|
68
|
+
// and the position involves no theme tokens at all. That leaves this stylesheet
|
|
69
|
+
// with only what genuinely varies declaratively.
|
|
70
|
+
const styles = StyleSheet.create(() => ({
|
|
71
|
+
wrapper: {
|
|
72
|
+
position: "relative",
|
|
73
|
+
variants: {
|
|
74
|
+
// `alignSelf: 'flex-start'` hugs the child, so the indicator anchors to
|
|
75
|
+
// the child's bounds rather than the surrounding container's. A child
|
|
76
|
+
// that was stretching needs `fullWidth` to go on doing so.
|
|
77
|
+
fullWidth: {
|
|
78
|
+
true: {
|
|
79
|
+
alignSelf: "stretch"
|
|
80
|
+
},
|
|
81
|
+
false: {
|
|
82
|
+
alignSelf: "flex-start"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
uni__dependencies: [4]
|
|
87
|
+
},
|
|
88
|
+
anchor: {
|
|
89
|
+
position: "absolute"
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
//# sourceMappingURL=Badge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","StyleSheet","BadgeIndicator","getAnchorPosition","jsx","_jsx","jsxs","_jsxs","Badge","children","placement","label","color","size","offset","isVisible","fullWidth","accessibilityLabel","style","rest","ref","_styles","styles","useVariants","View","wrapper","pointerEvents","anchor","create","position","variants","true","alignSelf","false","uni__dependencies"],"sourceRoot":"../../../../src","sources":["components/badge/Badge.tsx"],"mappings":";;;AAAA,SAA4BA,UAAU,QAAQ,OAAO;AAGrD,SAASC,UAAU,QAAQ,wBAAwB;AAEnD,SAASC,cAAc;AAEvB,SAASC,iBAAiB;;AAE1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAZA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAaA,MAAMC,KAAK,gBAAGR,UAAU,CAAwC,SAASQ,KAAKA,CAC5E;EACEC,QAAQ;EACRC,SAAS,GAAG,WAAW;EACvBC,KAAK;EACLC,KAAK,GAAG,QAAQ;EAChBC,IAAI,GAAG,IAAI;EACXC,MAAM,GAAG,CAAC;EACVC,SAAS,GAAG,IAAI;EAChBC,SAAS,GAAG,KAAK;EACjBC,kBAAkB;EAClBC,KAAK;EACL,GAAGC;AACL,CAAC,EACDC,GAAG,EACH;EAAA,MAAAC,OAAA,GAAAC,MAAA;EAAA;IAAA,MAAAA,MAAA,GAAAD,OAAA,CAAAE,WAAA,CACmB;MAAEP;IAAU,CAAC;IAEhC;MAAA;MACE;MACAT,KAAA,CAACiB,IAAI;QAACJ,GAAG,EAAEA,GAAI;QAACF,KAAK,EAAE,CAACI,MAAM,CAACG,OAAO,EAAEP,KAAK,CAAE;QAAA,GAAKC,IAAI;QAAAV,QAAA,GACrDA,QAAQ,EACRM,SAAS,gBACRV,IAAA,CAACmB,IAAI;UAACE,aAAa,EAAC,UAAU;UAACR,KAAK,EAAE,CAACI,MAAM,CAACK,MAAM,EAAExB,iBAAiB,CAACO,SAAS,EAAEG,IAAI,EAAEC,MAAM,CAAC,CAAE;UAAAL,QAAA,eAChGJ,IAAA,CAACH,cAAc;YAACS,KAAK,EAAEA,KAAM;YAACC,KAAK,EAAEA,KAAM;YAACC,IAAI,EAAEA,IAAK;YAACI,kBAAkB,EAAEA;UAAmB,CAAE;QAAC,CAC9F,CAAC,GACL,IAAI;MAAA,CACJ;IAAC;EACP;AACJ,CAAC,CAAC;AAEF,SAAST,KAAK;;AAEd;AACA;AACA;AACA;AACA;AACA;AACA,MAAMc,MAAM,GAAGrB,UAAU,CAAC2B,MAAM,CAAC,OAAO;EACtCH,OAAO,EAAE;IACPI,QAAQ,EAAE,UAAU;IACpBC,QAAQ,EAAE;MACR;MACA;MACA;MACAd,SAAS,EAAE;QACTe,IAAI,EAAE;UAAEC,SAAS,EAAE;QAAU,CAAC;QAC9BC,KAAK,EAAE;UAAED,SAAS,EAAE;QAAa;MACnC;IACF,CAAC;IAAAE,iBAAA;EACH,CAAC;EACDP,MAAM,EAAE;IACNE,QAAQ,EAAE;EACZ;AACF,CAAC,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["Badge"],"sourceRoot":"../../../../src","sources":["components/badge/index.ts"],"mappings":";;AAAA,SAASA,KAAK","ignoreList":[]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
4
|
+
import { Text as RNText } from "react-native-unistyles/components/native/Text";
|
|
5
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
6
|
+
import { resolveThemeColor } from "../../../utils/resolve-theme-color.js";
|
|
7
|
+
import { FONT_WEIGHT_VALUE, SYSTEM_FONT } from "../../../utils/typography.js";
|
|
8
|
+
import { getCustomBadgeColors, hasLabel } from "../utils.js";
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
const SCHEME_NAMES = new Set(["primary", "secondary", "success", "warning", "danger"]);
|
|
11
|
+
/** Label foreground per scheme — mirrors the spec's token table. */
|
|
12
|
+
const FG_TOKEN = {
|
|
13
|
+
primary: "accent.solid.fg",
|
|
14
|
+
secondary: "neutral.solid.fg",
|
|
15
|
+
success: "success.solid.fg",
|
|
16
|
+
warning: "warning.solid.fg",
|
|
17
|
+
danger: "danger.solid.fg"
|
|
18
|
+
};
|
|
19
|
+
const LABEL_VARIANT = {
|
|
20
|
+
sm: "label-sm",
|
|
21
|
+
md: "label-md",
|
|
22
|
+
lg: "label-lg"
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The overlay itself: a dot, or a pill carrying the label. Owns the paint, the
|
|
26
|
+
* legibility ring, and the accessibility node — the wrapper owns only position.
|
|
27
|
+
*
|
|
28
|
+
* `useUnistyles` is called unconditionally, which is what subscribes this
|
|
29
|
+
* component to theme changes. The background below comes from a variant, which
|
|
30
|
+
* unistyles applies straight to the native node on a theme switch; without a
|
|
31
|
+
* re-render React would go on believing the node holds the previous theme's
|
|
32
|
+
* value. `Chip` documents the same hazard on its root.
|
|
33
|
+
*/
|
|
34
|
+
export const BadgeIndicator = ({
|
|
35
|
+
label,
|
|
36
|
+
color,
|
|
37
|
+
size,
|
|
38
|
+
accessibilityLabel
|
|
39
|
+
}) => {
|
|
40
|
+
const {
|
|
41
|
+
theme
|
|
42
|
+
} = useUnistyles();
|
|
43
|
+
// A literal never matches one of the five variant options, so it's cast to
|
|
44
|
+
// satisfy `useVariants`'s literal-union type while still being compared
|
|
45
|
+
// against the real value below.
|
|
46
|
+
const _styles = styles;
|
|
47
|
+
{
|
|
48
|
+
const styles = _styles.useVariants({
|
|
49
|
+
color: color,
|
|
50
|
+
size
|
|
51
|
+
});
|
|
52
|
+
const custom = SCHEME_NAMES.has(color) ? null : getCustomBadgeColors(color);
|
|
53
|
+
const labelled = hasLabel(label);
|
|
54
|
+
const foreground = custom ? custom.fg : FG_TOKEN[color];
|
|
55
|
+
|
|
56
|
+
// An explicit label wins even on a dot; a dot with nothing to announce is
|
|
57
|
+
// decorative and hidden rather than read out as an unlabelled element.
|
|
58
|
+
const announced = accessibilityLabel ?? (labelled ? String(label) : undefined);
|
|
59
|
+
const accessibility = announced ? {
|
|
60
|
+
accessible: true,
|
|
61
|
+
accessibilityLabel: announced
|
|
62
|
+
} : {
|
|
63
|
+
accessibilityElementsHidden: true,
|
|
64
|
+
importantForAccessibility: "no-hide-descendants",
|
|
65
|
+
"aria-hidden": true
|
|
66
|
+
};
|
|
67
|
+
return /*#__PURE__*/_jsx(View, {
|
|
68
|
+
style: [styles.indicator, labelled ? styles.pill : styles.dot, custom ? {
|
|
69
|
+
backgroundColor: custom.bg
|
|
70
|
+
} : null],
|
|
71
|
+
...accessibility,
|
|
72
|
+
children: labelled ? /*#__PURE__*/_jsx(RNText, {
|
|
73
|
+
style: [styles.label, {
|
|
74
|
+
color: resolveThemeColor(foreground, theme)
|
|
75
|
+
}],
|
|
76
|
+
maxFontSizeMultiplier: theme.typography.variants[LABEL_VARIANT[size]].maxFontSizeMultiplier,
|
|
77
|
+
numberOfLines: 1,
|
|
78
|
+
children: label
|
|
79
|
+
}) : null
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// `color` is a plain variant rather than a compound one: `Badge` renders solid
|
|
87
|
+
// only, so there is no second paint axis for it to interact with.
|
|
88
|
+
//
|
|
89
|
+
// React Native's box model is border-box, so the ring below eats into the
|
|
90
|
+
// indicator rather than growing it — the dot's outer diameter stays exactly
|
|
91
|
+
// what `size` declares, with no compensation arithmetic.
|
|
92
|
+
const styles = StyleSheet.create(theme => {
|
|
93
|
+
const c = theme.color;
|
|
94
|
+
// A custom font encodes its weight in the family name, so only the system
|
|
95
|
+
// font takes a numeric `fontWeight` — the same branch `Chip` makes. Inlined
|
|
96
|
+
// rather than extracted: the unistyles Babel plugin rewrites this factory,
|
|
97
|
+
// and an imported function called from inside it is not in scope at runtime.
|
|
98
|
+
const labelFont = theme.typography.fonts.semibold;
|
|
99
|
+
const isSystemLabelFont = labelFont === SYSTEM_FONT;
|
|
100
|
+
return {
|
|
101
|
+
indicator: {
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
justifyContent: "center",
|
|
104
|
+
borderRadius: theme.radius.full,
|
|
105
|
+
borderColor: c["surface.canvas"],
|
|
106
|
+
variants: {
|
|
107
|
+
size: {
|
|
108
|
+
sm: {
|
|
109
|
+
borderWidth: 1.5
|
|
110
|
+
},
|
|
111
|
+
md: {
|
|
112
|
+
borderWidth: 2
|
|
113
|
+
},
|
|
114
|
+
lg: {
|
|
115
|
+
borderWidth: 2
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
color: {
|
|
119
|
+
primary: {
|
|
120
|
+
backgroundColor: c["accent.solid.bg"]
|
|
121
|
+
},
|
|
122
|
+
secondary: {
|
|
123
|
+
backgroundColor: c["neutral.solid.bg"]
|
|
124
|
+
},
|
|
125
|
+
success: {
|
|
126
|
+
backgroundColor: c["success.solid.bg"]
|
|
127
|
+
},
|
|
128
|
+
warning: {
|
|
129
|
+
backgroundColor: c["warning.solid.bg"]
|
|
130
|
+
},
|
|
131
|
+
danger: {
|
|
132
|
+
backgroundColor: c["danger.solid.bg"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
uni__dependencies: [0, 4]
|
|
137
|
+
},
|
|
138
|
+
// These diameters are mirrored by `DOT_SIZE` in `../utils.ts`, which the
|
|
139
|
+
// anchor arithmetic reads. The Babel plugin needs them literal here, so a
|
|
140
|
+
// test asserts the two stay equal.
|
|
141
|
+
dot: {
|
|
142
|
+
variants: {
|
|
143
|
+
size: {
|
|
144
|
+
sm: {
|
|
145
|
+
width: 12,
|
|
146
|
+
height: 12
|
|
147
|
+
},
|
|
148
|
+
md: {
|
|
149
|
+
width: 16,
|
|
150
|
+
height: 16
|
|
151
|
+
},
|
|
152
|
+
lg: {
|
|
153
|
+
width: 20,
|
|
154
|
+
height: 20
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
uni__dependencies: [4]
|
|
159
|
+
},
|
|
160
|
+
pill: {
|
|
161
|
+
variants: {
|
|
162
|
+
size: {
|
|
163
|
+
sm: {
|
|
164
|
+
height: 18,
|
|
165
|
+
minWidth: 18,
|
|
166
|
+
paddingHorizontal: 5
|
|
167
|
+
},
|
|
168
|
+
md: {
|
|
169
|
+
height: 22,
|
|
170
|
+
minWidth: 22,
|
|
171
|
+
paddingHorizontal: 6
|
|
172
|
+
},
|
|
173
|
+
lg: {
|
|
174
|
+
height: 26,
|
|
175
|
+
minWidth: 26,
|
|
176
|
+
paddingHorizontal: 8
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
uni__dependencies: [4]
|
|
181
|
+
},
|
|
182
|
+
// Spelled out rather than derived from `LABEL_VARIANT`: the unistyles Babel
|
|
183
|
+
// plugin reads this object statically, so computed variant keys are not
|
|
184
|
+
// safe here even though they would resolve correctly under test.
|
|
185
|
+
label: {
|
|
186
|
+
textAlign: "center",
|
|
187
|
+
fontFamily: isSystemLabelFont ? undefined : labelFont,
|
|
188
|
+
fontWeight: isSystemLabelFont ? FONT_WEIGHT_VALUE.semibold : undefined,
|
|
189
|
+
variants: {
|
|
190
|
+
size: {
|
|
191
|
+
sm: {
|
|
192
|
+
fontSize: theme.typography.variants["label-sm"].fontSize,
|
|
193
|
+
lineHeight: theme.typography.variants["label-sm"].lineHeight,
|
|
194
|
+
letterSpacing: theme.typography.variants["label-sm"].letterSpacing
|
|
195
|
+
},
|
|
196
|
+
md: {
|
|
197
|
+
fontSize: theme.typography.variants["label-md"].fontSize,
|
|
198
|
+
lineHeight: theme.typography.variants["label-md"].lineHeight,
|
|
199
|
+
letterSpacing: theme.typography.variants["label-md"].letterSpacing
|
|
200
|
+
},
|
|
201
|
+
lg: {
|
|
202
|
+
fontSize: theme.typography.variants["label-lg"].fontSize,
|
|
203
|
+
lineHeight: theme.typography.variants["label-lg"].lineHeight,
|
|
204
|
+
letterSpacing: theme.typography.variants["label-lg"].letterSpacing
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
uni__dependencies: [0, 4]
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=BadgeIndicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["StyleSheet","useUnistyles","resolveThemeColor","FONT_WEIGHT_VALUE","SYSTEM_FONT","getCustomBadgeColors","hasLabel","jsx","_jsx","SCHEME_NAMES","Set","FG_TOKEN","primary","secondary","success","warning","danger","LABEL_VARIANT","sm","md","lg","BadgeIndicator","label","color","size","accessibilityLabel","theme","_styles","styles","useVariants","custom","has","labelled","foreground","fg","announced","String","undefined","accessibility","accessible","accessibilityElementsHidden","importantForAccessibility","View","style","indicator","pill","dot","backgroundColor","bg","children","RNText","maxFontSizeMultiplier","typography","variants","numberOfLines","create","c","labelFont","fonts","semibold","isSystemLabelFont","alignItems","justifyContent","borderRadius","radius","full","borderColor","borderWidth","uni__dependencies","width","height","minWidth","paddingHorizontal","textAlign","fontFamily","fontWeight","fontSize","lineHeight","letterSpacing"],"sourceRoot":"../../../../../src","sources":["components/badge/parts/BadgeIndicator.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../../src","sources":["components/badge/types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { readableOn } from "../../utils/color-utils.js";
|
|
4
|
+
|
|
5
|
+
// A custom color literal (hex/rgb/hsl) isn't a bounded set of options, so it
|
|
6
|
+
// can't be a unistyles variant condition — it's derived here instead, the same
|
|
7
|
+
// way `chip/utils.ts` handles the equivalent case on `Chip`.
|
|
8
|
+
|
|
9
|
+
/** The indicator's paint for a raw literal: the literal itself, plus a readable label. */
|
|
10
|
+
export function getCustomBadgeColors(literal) {
|
|
11
|
+
return {
|
|
12
|
+
bg: literal,
|
|
13
|
+
fg: readableOn(literal)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `true` when `label` should render as a pill. An empty string and `undefined`
|
|
19
|
+
* are a dot; `0` is a label, since a caller suppressing a zero count uses
|
|
20
|
+
* `isVisible` rather than relying on falsiness.
|
|
21
|
+
*/
|
|
22
|
+
export function hasLabel(label) {
|
|
23
|
+
return label !== undefined && label !== "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Outer diameter of the dot per size. The indicator's own stylesheet repeats
|
|
28
|
+
* these as literals — the unistyles Babel plugin reads that object statically,
|
|
29
|
+
* so it cannot reference this table — and `anchorOffsetsMatchDotRadius` in the
|
|
30
|
+
* test suite is what keeps the two in step.
|
|
31
|
+
*/
|
|
32
|
+
export const DOT_SIZE = {
|
|
33
|
+
sm: 12,
|
|
34
|
+
md: 16,
|
|
35
|
+
lg: 20
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Absolute insets for the indicator, in pure arithmetic rather than one
|
|
39
|
+
* compound variant per (placement, size) cell: `offset` is an open-ended number
|
|
40
|
+
* and could never be a variant condition, and nothing here touches the theme.
|
|
41
|
+
*
|
|
42
|
+
* The base inset is the dot's radius, which puts a dot's centre exactly on the
|
|
43
|
+
* child's bounding-box corner. `offset` moves it from there — positive away
|
|
44
|
+
* from the child's centre, negative toward it.
|
|
45
|
+
*/
|
|
46
|
+
export function getAnchorPosition(placement, size, offset = 0) {
|
|
47
|
+
const base = DOT_SIZE[size] / 2;
|
|
48
|
+
const {
|
|
49
|
+
x,
|
|
50
|
+
y
|
|
51
|
+
} = typeof offset === "number" ? {
|
|
52
|
+
x: offset,
|
|
53
|
+
y: offset
|
|
54
|
+
} : {
|
|
55
|
+
x: offset.x ?? 0,
|
|
56
|
+
y: offset.y ?? 0
|
|
57
|
+
};
|
|
58
|
+
const insetX = -(base + x);
|
|
59
|
+
const insetY = -(base + y);
|
|
60
|
+
switch (placement) {
|
|
61
|
+
case "top-left":
|
|
62
|
+
return {
|
|
63
|
+
top: insetY,
|
|
64
|
+
left: insetX
|
|
65
|
+
};
|
|
66
|
+
case "bottom-right":
|
|
67
|
+
return {
|
|
68
|
+
bottom: insetY,
|
|
69
|
+
right: insetX
|
|
70
|
+
};
|
|
71
|
+
case "bottom-left":
|
|
72
|
+
return {
|
|
73
|
+
bottom: insetY,
|
|
74
|
+
left: insetX
|
|
75
|
+
};
|
|
76
|
+
default:
|
|
77
|
+
return {
|
|
78
|
+
top: insetY,
|
|
79
|
+
right: insetX
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["readableOn","getCustomBadgeColors","literal","bg","fg","hasLabel","label","undefined","DOT_SIZE","sm","md","lg","getAnchorPosition","placement","size","offset","base","x","y","insetX","insetY","top","left","bottom","right"],"sourceRoot":"../../../../src","sources":["components/badge/utils.ts"],"mappings":";;AAAA,SAASA,UAAU;;AAGnB;AACA;AACA;;AAOA;AACA,OAAO,SAASC,oBAAoBA,CAACC,OAAe,EAAqB;EACvE,OAAO;IAAEC,EAAE,EAAED,OAAO;IAAEE,EAAE,EAAEJ,UAAU,CAACE,OAAO;EAAE,CAAC;AACjD;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,QAAQA,CAACC,KAAkC,EAA4B;EACrF,OAAOA,KAAK,KAAKC,SAAS,IAAID,KAAK,KAAK,EAAE;AAC5C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,QAAmC,GAAG;EACjDC,EAAE,EAAE,EAAE;EACNC,EAAE,EAAE,EAAE;EACNC,EAAE,EAAE;AACN,CAAC;AASD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAC/BC,SAAyB,EACzBC,IAAe,EACfC,MAA2C,GAAG,CAAC,EAC/B;EAChB,MAAMC,IAAI,GAAGR,QAAQ,CAACM,IAAI,CAAC,GAAG,CAAC;EAC/B,MAAM;IAAEG,CAAC;IAAEC;EAAE,CAAC,GACZ,OAAOH,MAAM,KAAK,QAAQ,GAAG;IAAEE,CAAC,EAAEF,MAAM;IAAEG,CAAC,EAAEH;EAAO,CAAC,GAAG;IAAEE,CAAC,EAAEF,MAAM,CAACE,CAAC,IAAI,CAAC;IAAEC,CAAC,EAAEH,MAAM,CAACG,CAAC,IAAI;EAAE,CAAC;EAEhG,MAAMC,MAAM,GAAG,EAAEH,IAAI,GAAGC,CAAC,CAAC;EAC1B,MAAMG,MAAM,GAAG,EAAEJ,IAAI,GAAGE,CAAC,CAAC;EAE1B,QAAQL,SAAS;IACf,KAAK,UAAU;MACb,OAAO;QAAEQ,GAAG,EAAED,MAAM;QAAEE,IAAI,EAAEH;MAAO,CAAC;IACtC,KAAK,cAAc;MACjB,OAAO;QAAEI,MAAM,EAAEH,MAAM;QAAEI,KAAK,EAAEL;MAAO,CAAC;IAC1C,KAAK,aAAa;MAChB,OAAO;QAAEI,MAAM,EAAEH,MAAM;QAAEE,IAAI,EAAEH;MAAO,CAAC;IACzC;MACE,OAAO;QAAEE,GAAG,EAAED,MAAM;QAAEI,KAAK,EAAEL;MAAO,CAAC;EACzC;AACF","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["components/index.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["components/index.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BadgeProps } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Overlays a count or status indicator on one corner of the element it wraps.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <Badge placement="top-right" color="danger" label={3}>
|
|
7
|
+
* <Toolbar.Button icon="bell" accessibilityLabel="Notifications" />
|
|
8
|
+
* </Badge>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The child renders untouched — `Badge` contributes the positioning context and
|
|
12
|
+
* the overlay, nothing else. A round child usually wants a negative `offset`,
|
|
13
|
+
* since the corner being anchored to is the bounding box's, not the circle's.
|
|
14
|
+
*/
|
|
15
|
+
declare const Badge: import("react").ForwardRefExoticComponent<BadgeProps & import("react").RefAttributes<import("react-native/types_generated/src/private/webapis/dom/nodes/ReactNativeElement").default>>;
|
|
16
|
+
export { Badge };
|
|
17
|
+
//# sourceMappingURL=Badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../../../../src/components/badge/Badge.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAS,CAAC;AAG1C;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,KAAK,wLA6BT,CAAC;AAEH,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/badge/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAS,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BadgeColor, BadgeSize } from "../types.js";
|
|
2
|
+
export interface BadgeIndicatorProps {
|
|
3
|
+
label?: string | number;
|
|
4
|
+
color: BadgeColor;
|
|
5
|
+
size: BadgeSize;
|
|
6
|
+
accessibilityLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The overlay itself: a dot, or a pill carrying the label. Owns the paint, the
|
|
10
|
+
* legibility ring, and the accessibility node — the wrapper owns only position.
|
|
11
|
+
*
|
|
12
|
+
* `useUnistyles` is called unconditionally, which is what subscribes this
|
|
13
|
+
* component to theme changes. The background below comes from a variant, which
|
|
14
|
+
* unistyles applies straight to the native node on a theme switch; without a
|
|
15
|
+
* re-render React would go on believing the node holds the previous theme's
|
|
16
|
+
* value. `Chip` documents the same hazard on its root.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BadgeIndicator: ({ label, color, size, accessibilityLabel }: BadgeIndicatorProps) => import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=BadgeIndicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BadgeIndicator.d.ts","sourceRoot":"","sources":["../../../../../../src/components/badge/parts/BadgeIndicator.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAU,CAAC;AAsBtD,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,4CAA4C,mBAAmB,gCA0C7F,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
/** Corner of the wrapped child the indicator is anchored to. */
|
|
4
|
+
export type BadgePlacement = "top-right" | "top-left" | "bottom-right" | "bottom-left";
|
|
5
|
+
/** Scale of the dot, pill, and label. */
|
|
6
|
+
export type BadgeSize = "sm" | "md" | "lg";
|
|
7
|
+
/**
|
|
8
|
+
* Semantic color scheme, or a raw CSS color literal (`#fff`, `rgb(...)`, `hsl(...)`).
|
|
9
|
+
* A literal paints the indicator with that exact value and derives a readable
|
|
10
|
+
* foreground for the label.
|
|
11
|
+
*/
|
|
12
|
+
export type BadgeColor = "primary" | "secondary" | "success" | "warning" | "danger" | (string & {});
|
|
13
|
+
export interface BadgeProps extends ViewProps {
|
|
14
|
+
/** The element being badged. Rendered untouched; `Badge` only adds the overlay. */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Corner the indicator sits on. Defaults to `'top-right'`.
|
|
18
|
+
*
|
|
19
|
+
* The indicator overhangs the child, so an ancestor that clips — a `Surface`
|
|
20
|
+
* with a radius, or a `Screen` list row — will cut it off. Pad that container
|
|
21
|
+
* by the overhang, or lift the `Badge` outside it.
|
|
22
|
+
*/
|
|
23
|
+
placement?: BadgePlacement;
|
|
24
|
+
/**
|
|
25
|
+
* Text in the indicator. Omit it (or pass an empty string) for a dot.
|
|
26
|
+
* `0` renders as a label — use `isVisible` to suppress a zero count.
|
|
27
|
+
*/
|
|
28
|
+
label?: string | number;
|
|
29
|
+
/** Semantic scheme or raw color literal. Defaults to `'danger'`. */
|
|
30
|
+
color?: BadgeColor;
|
|
31
|
+
/** Scale of the indicator. Defaults to `'md'`. */
|
|
32
|
+
size?: BadgeSize;
|
|
33
|
+
/**
|
|
34
|
+
* Nudges the indicator along both axes from the corner `placement` picked.
|
|
35
|
+
* Positive moves it away from the child's centre, negative pulls it in.
|
|
36
|
+
* A number applies to both axes; `{ x, y }` sets them separately.
|
|
37
|
+
*
|
|
38
|
+
* The corner it anchors to is the child's **bounding box**, so a round child
|
|
39
|
+
* needs a negative offset to bring the indicator onto the visible edge — a
|
|
40
|
+
* circle only meets its box at 45°, about `0.21 × width` away. Defaults to `0`.
|
|
41
|
+
*/
|
|
42
|
+
offset?: number | {
|
|
43
|
+
x?: number;
|
|
44
|
+
y?: number;
|
|
45
|
+
};
|
|
46
|
+
/** Renders the indicator. The child renders either way. Defaults to `true`. */
|
|
47
|
+
isVisible?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Stretches the wrapper along its parent's cross axis, so a child that
|
|
50
|
+
* stretches unwrapped keeps stretching. Defaults to `false`, which hugs the
|
|
51
|
+
* child — what an icon, avatar, or toolbar button needs. An explicit
|
|
52
|
+
* `alignSelf` in `style` beats both.
|
|
53
|
+
*/
|
|
54
|
+
fullWidth?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Announced in place of the label. Defaults to the label's own text; an
|
|
57
|
+
* unlabelled dot is hidden from assistive technology.
|
|
58
|
+
*/
|
|
59
|
+
accessibilityLabel?: string;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/components/badge/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AAEvF,yCAAyC;AACzC,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,WAAW,GACX,SAAS,GACT,SAAS,GACT,QAAQ,GAER,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C,mFAAmF;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,oEAAoE;IACpE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,kDAAkD;IAClD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,+EAA+E;IAC/E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BadgePlacement, BadgeSize } from "./types.js";
|
|
2
|
+
export interface CustomBadgeColors {
|
|
3
|
+
bg: string;
|
|
4
|
+
fg: string;
|
|
5
|
+
}
|
|
6
|
+
/** The indicator's paint for a raw literal: the literal itself, plus a readable label. */
|
|
7
|
+
export declare function getCustomBadgeColors(literal: string): CustomBadgeColors;
|
|
8
|
+
/**
|
|
9
|
+
* `true` when `label` should render as a pill. An empty string and `undefined`
|
|
10
|
+
* are a dot; `0` is a label, since a caller suppressing a zero count uses
|
|
11
|
+
* `isVisible` rather than relying on falsiness.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hasLabel(label: string | number | undefined): label is string | number;
|
|
14
|
+
/**
|
|
15
|
+
* Outer diameter of the dot per size. The indicator's own stylesheet repeats
|
|
16
|
+
* these as literals — the unistyles Babel plugin reads that object statically,
|
|
17
|
+
* so it cannot reference this table — and `anchorOffsetsMatchDotRadius` in the
|
|
18
|
+
* test suite is what keeps the two in step.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DOT_SIZE: Record<BadgeSize, number>;
|
|
21
|
+
export interface AnchorPosition {
|
|
22
|
+
top?: number;
|
|
23
|
+
right?: number;
|
|
24
|
+
bottom?: number;
|
|
25
|
+
left?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Absolute insets for the indicator, in pure arithmetic rather than one
|
|
29
|
+
* compound variant per (placement, size) cell: `offset` is an open-ended number
|
|
30
|
+
* and could never be a variant condition, and nothing here touches the theme.
|
|
31
|
+
*
|
|
32
|
+
* The base inset is the dot's radius, which puts a dot's centre exactly on the
|
|
33
|
+
* child's bounding-box corner. `offset` moves it from there — positive away
|
|
34
|
+
* from the child's centre, negative toward it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getAnchorPosition(placement: BadgePlacement, size: BadgeSize, offset?: number | {
|
|
37
|
+
x?: number;
|
|
38
|
+
y?: number;
|
|
39
|
+
}): AnchorPosition;
|
|
40
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/components/badge/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAS,CAAC;AAMzD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEvE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,CAErF;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAI9C,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,cAAc,EACzB,IAAI,EAAE,SAAS,EACf,MAAM,GAAE,MAAM,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAM,GAC9C,cAAc,CAkBhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAU,CAAC;AACzB,cAAc,gBAAO,CAAC;AACtB,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,oBAAW,CAAC;AAC1B,cAAc,wBAAe,CAAC;AAC9B,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,kBAAS,CAAC;AACxB,cAAc,2BAAkB,CAAC;AACjC,cAAc,mBAAU,CAAC;AACzB,cAAc,sBAAa,CAAC;AAC5B,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,oBAAW,CAAC;AAC1B,cAAc,oBAAW,CAAC;AAC1B,cAAc,iBAAQ,CAAC;AACvB,cAAc,kBAAS,CAAC;AACxB,cAAc,oBAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAU,CAAC;AACzB,cAAc,kBAAS,CAAC;AACxB,cAAc,gBAAO,CAAC;AACtB,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,oBAAW,CAAC;AAC1B,cAAc,wBAAe,CAAC;AAC9B,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,kBAAS,CAAC;AACxB,cAAc,2BAAkB,CAAC;AACjC,cAAc,mBAAU,CAAC;AACzB,cAAc,sBAAa,CAAC;AAC5B,cAAc,mBAAU,CAAC;AACzB,cAAc,iBAAQ,CAAC;AACvB,cAAc,oBAAW,CAAC;AAC1B,cAAc,oBAAW,CAAC;AAC1B,cAAc,iBAAQ,CAAC;AACvB,cAAc,kBAAS,CAAC;AACxB,cAAc,oBAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ComponentRef, forwardRef } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
5
|
+
|
|
6
|
+
import { BadgeIndicator } from "./parts/BadgeIndicator";
|
|
7
|
+
import type { BadgeProps } from "./types";
|
|
8
|
+
import { getAnchorPosition } from "./utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Overlays a count or status indicator on one corner of the element it wraps.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Badge placement="top-right" color="danger" label={3}>
|
|
15
|
+
* <Toolbar.Button icon="bell" accessibilityLabel="Notifications" />
|
|
16
|
+
* </Badge>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The child renders untouched — `Badge` contributes the positioning context and
|
|
20
|
+
* the overlay, nothing else. A round child usually wants a negative `offset`,
|
|
21
|
+
* since the corner being anchored to is the bounding box's, not the circle's.
|
|
22
|
+
*/
|
|
23
|
+
const Badge = forwardRef<ComponentRef<typeof View>, BadgeProps>(function Badge(
|
|
24
|
+
{
|
|
25
|
+
children,
|
|
26
|
+
placement = "top-right",
|
|
27
|
+
label,
|
|
28
|
+
color = "danger",
|
|
29
|
+
size = "md",
|
|
30
|
+
offset = 0,
|
|
31
|
+
isVisible = true,
|
|
32
|
+
fullWidth = false,
|
|
33
|
+
accessibilityLabel,
|
|
34
|
+
style,
|
|
35
|
+
...rest
|
|
36
|
+
},
|
|
37
|
+
ref
|
|
38
|
+
) {
|
|
39
|
+
styles.useVariants({ fullWidth });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
// `style` last so an explicit `alignSelf` beats `fullWidth`.
|
|
43
|
+
<View ref={ref} style={[styles.wrapper, style]} {...rest}>
|
|
44
|
+
{children}
|
|
45
|
+
{isVisible ? (
|
|
46
|
+
<View pointerEvents="box-none" style={[styles.anchor, getAnchorPosition(placement, size, offset)]}>
|
|
47
|
+
<BadgeIndicator label={label} color={color} size={size} accessibilityLabel={accessibilityLabel} />
|
|
48
|
+
</View>
|
|
49
|
+
) : null}
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export { Badge };
|
|
55
|
+
|
|
56
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
57
|
+
//
|
|
58
|
+
// The indicator's insets are computed in `getAnchorPosition`, not declared here:
|
|
59
|
+
// `offset` is an open-ended number, so it could never be a variant condition,
|
|
60
|
+
// and the position involves no theme tokens at all. That leaves this stylesheet
|
|
61
|
+
// with only what genuinely varies declaratively.
|
|
62
|
+
const styles = StyleSheet.create(() => ({
|
|
63
|
+
wrapper: {
|
|
64
|
+
position: "relative",
|
|
65
|
+
variants: {
|
|
66
|
+
// `alignSelf: 'flex-start'` hugs the child, so the indicator anchors to
|
|
67
|
+
// the child's bounds rather than the surrounding container's. A child
|
|
68
|
+
// that was stretching needs `fullWidth` to go on doing so.
|
|
69
|
+
fullWidth: {
|
|
70
|
+
true: { alignSelf: "stretch" },
|
|
71
|
+
false: { alignSelf: "flex-start" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
anchor: {
|
|
76
|
+
position: "absolute",
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Text as RNText, View } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
4
|
+
|
|
5
|
+
import type { ColorToken, RoleTextVariant } from "#theme/types";
|
|
6
|
+
import { resolveThemeColor } from "#utils/resolve-theme-color";
|
|
7
|
+
import { FONT_WEIGHT_VALUE, SYSTEM_FONT } from "#utils/typography";
|
|
8
|
+
import type { BadgeColor, BadgeSize } from "../types";
|
|
9
|
+
import { getCustomBadgeColors, hasLabel } from "../utils";
|
|
10
|
+
|
|
11
|
+
const SCHEME_NAMES = new Set(["primary", "secondary", "success", "warning", "danger"]);
|
|
12
|
+
|
|
13
|
+
type BadgeScheme = "primary" | "secondary" | "success" | "warning" | "danger";
|
|
14
|
+
|
|
15
|
+
/** Label foreground per scheme — mirrors the spec's token table. */
|
|
16
|
+
const FG_TOKEN: Record<BadgeScheme, ColorToken> = {
|
|
17
|
+
primary: "accent.solid.fg",
|
|
18
|
+
secondary: "neutral.solid.fg",
|
|
19
|
+
success: "success.solid.fg",
|
|
20
|
+
warning: "warning.solid.fg",
|
|
21
|
+
danger: "danger.solid.fg",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const LABEL_VARIANT: Record<BadgeSize, RoleTextVariant> = {
|
|
25
|
+
sm: "label-sm",
|
|
26
|
+
md: "label-md",
|
|
27
|
+
lg: "label-lg",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface BadgeIndicatorProps {
|
|
31
|
+
label?: string | number;
|
|
32
|
+
color: BadgeColor;
|
|
33
|
+
size: BadgeSize;
|
|
34
|
+
accessibilityLabel?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The overlay itself: a dot, or a pill carrying the label. Owns the paint, the
|
|
39
|
+
* legibility ring, and the accessibility node — the wrapper owns only position.
|
|
40
|
+
*
|
|
41
|
+
* `useUnistyles` is called unconditionally, which is what subscribes this
|
|
42
|
+
* component to theme changes. The background below comes from a variant, which
|
|
43
|
+
* unistyles applies straight to the native node on a theme switch; without a
|
|
44
|
+
* re-render React would go on believing the node holds the previous theme's
|
|
45
|
+
* value. `Chip` documents the same hazard on its root.
|
|
46
|
+
*/
|
|
47
|
+
export const BadgeIndicator = ({ label, color, size, accessibilityLabel }: BadgeIndicatorProps) => {
|
|
48
|
+
const { theme } = useUnistyles();
|
|
49
|
+
// A literal never matches one of the five variant options, so it's cast to
|
|
50
|
+
// satisfy `useVariants`'s literal-union type while still being compared
|
|
51
|
+
// against the real value below.
|
|
52
|
+
styles.useVariants({ color: color as BadgeScheme, size });
|
|
53
|
+
|
|
54
|
+
const custom = SCHEME_NAMES.has(color) ? null : getCustomBadgeColors(color);
|
|
55
|
+
const labelled = hasLabel(label);
|
|
56
|
+
const foreground = custom ? custom.fg : FG_TOKEN[color as BadgeScheme];
|
|
57
|
+
|
|
58
|
+
// An explicit label wins even on a dot; a dot with nothing to announce is
|
|
59
|
+
// decorative and hidden rather than read out as an unlabelled element.
|
|
60
|
+
const announced = accessibilityLabel ?? (labelled ? String(label) : undefined);
|
|
61
|
+
const accessibility = announced
|
|
62
|
+
? { accessible: true, accessibilityLabel: announced }
|
|
63
|
+
: {
|
|
64
|
+
accessibilityElementsHidden: true,
|
|
65
|
+
importantForAccessibility: "no-hide-descendants" as const,
|
|
66
|
+
"aria-hidden": true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View
|
|
71
|
+
style={[
|
|
72
|
+
styles.indicator,
|
|
73
|
+
labelled ? styles.pill : styles.dot,
|
|
74
|
+
custom ? { backgroundColor: custom.bg } : null,
|
|
75
|
+
]}
|
|
76
|
+
{...accessibility}
|
|
77
|
+
>
|
|
78
|
+
{labelled ? (
|
|
79
|
+
<RNText
|
|
80
|
+
style={[styles.label, { color: resolveThemeColor(foreground, theme) }]}
|
|
81
|
+
maxFontSizeMultiplier={theme.typography.variants[LABEL_VARIANT[size]].maxFontSizeMultiplier}
|
|
82
|
+
numberOfLines={1}
|
|
83
|
+
>
|
|
84
|
+
{label}
|
|
85
|
+
</RNText>
|
|
86
|
+
) : null}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
92
|
+
//
|
|
93
|
+
// `color` is a plain variant rather than a compound one: `Badge` renders solid
|
|
94
|
+
// only, so there is no second paint axis for it to interact with.
|
|
95
|
+
//
|
|
96
|
+
// React Native's box model is border-box, so the ring below eats into the
|
|
97
|
+
// indicator rather than growing it — the dot's outer diameter stays exactly
|
|
98
|
+
// what `size` declares, with no compensation arithmetic.
|
|
99
|
+
const styles = StyleSheet.create((theme) => {
|
|
100
|
+
const c = theme.color;
|
|
101
|
+
// A custom font encodes its weight in the family name, so only the system
|
|
102
|
+
// font takes a numeric `fontWeight` — the same branch `Chip` makes. Inlined
|
|
103
|
+
// rather than extracted: the unistyles Babel plugin rewrites this factory,
|
|
104
|
+
// and an imported function called from inside it is not in scope at runtime.
|
|
105
|
+
const labelFont = theme.typography.fonts.semibold;
|
|
106
|
+
const isSystemLabelFont = labelFont === SYSTEM_FONT;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
indicator: {
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "center",
|
|
112
|
+
borderRadius: theme.radius.full,
|
|
113
|
+
borderColor: c["surface.canvas"],
|
|
114
|
+
variants: {
|
|
115
|
+
size: {
|
|
116
|
+
sm: { borderWidth: 1.5 },
|
|
117
|
+
md: { borderWidth: 2 },
|
|
118
|
+
lg: { borderWidth: 2 },
|
|
119
|
+
},
|
|
120
|
+
color: {
|
|
121
|
+
primary: { backgroundColor: c["accent.solid.bg"] },
|
|
122
|
+
secondary: { backgroundColor: c["neutral.solid.bg"] },
|
|
123
|
+
success: { backgroundColor: c["success.solid.bg"] },
|
|
124
|
+
warning: { backgroundColor: c["warning.solid.bg"] },
|
|
125
|
+
danger: { backgroundColor: c["danger.solid.bg"] },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// These diameters are mirrored by `DOT_SIZE` in `../utils.ts`, which the
|
|
130
|
+
// anchor arithmetic reads. The Babel plugin needs them literal here, so a
|
|
131
|
+
// test asserts the two stay equal.
|
|
132
|
+
dot: {
|
|
133
|
+
variants: {
|
|
134
|
+
size: {
|
|
135
|
+
sm: { width: 12, height: 12 },
|
|
136
|
+
md: { width: 16, height: 16 },
|
|
137
|
+
lg: { width: 20, height: 20 },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
pill: {
|
|
142
|
+
variants: {
|
|
143
|
+
size: {
|
|
144
|
+
sm: { height: 18, minWidth: 18, paddingHorizontal: 5 },
|
|
145
|
+
md: { height: 22, minWidth: 22, paddingHorizontal: 6 },
|
|
146
|
+
lg: { height: 26, minWidth: 26, paddingHorizontal: 8 },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
// Spelled out rather than derived from `LABEL_VARIANT`: the unistyles Babel
|
|
151
|
+
// plugin reads this object statically, so computed variant keys are not
|
|
152
|
+
// safe here even though they would resolve correctly under test.
|
|
153
|
+
label: {
|
|
154
|
+
textAlign: "center",
|
|
155
|
+
fontFamily: isSystemLabelFont ? undefined : labelFont,
|
|
156
|
+
fontWeight: isSystemLabelFont ? FONT_WEIGHT_VALUE.semibold : undefined,
|
|
157
|
+
variants: {
|
|
158
|
+
size: {
|
|
159
|
+
sm: {
|
|
160
|
+
fontSize: theme.typography.variants["label-sm"].fontSize,
|
|
161
|
+
lineHeight: theme.typography.variants["label-sm"].lineHeight,
|
|
162
|
+
letterSpacing: theme.typography.variants["label-sm"].letterSpacing,
|
|
163
|
+
},
|
|
164
|
+
md: {
|
|
165
|
+
fontSize: theme.typography.variants["label-md"].fontSize,
|
|
166
|
+
lineHeight: theme.typography.variants["label-md"].lineHeight,
|
|
167
|
+
letterSpacing: theme.typography.variants["label-md"].letterSpacing,
|
|
168
|
+
},
|
|
169
|
+
lg: {
|
|
170
|
+
fontSize: theme.typography.variants["label-lg"].fontSize,
|
|
171
|
+
lineHeight: theme.typography.variants["label-lg"].lineHeight,
|
|
172
|
+
letterSpacing: theme.typography.variants["label-lg"].letterSpacing,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
/** Corner of the wrapped child the indicator is anchored to. */
|
|
5
|
+
export type BadgePlacement = "top-right" | "top-left" | "bottom-right" | "bottom-left";
|
|
6
|
+
|
|
7
|
+
/** Scale of the dot, pill, and label. */
|
|
8
|
+
export type BadgeSize = "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Semantic color scheme, or a raw CSS color literal (`#fff`, `rgb(...)`, `hsl(...)`).
|
|
12
|
+
* A literal paints the indicator with that exact value and derives a readable
|
|
13
|
+
* foreground for the label.
|
|
14
|
+
*/
|
|
15
|
+
export type BadgeColor =
|
|
16
|
+
| "primary"
|
|
17
|
+
| "secondary"
|
|
18
|
+
| "success"
|
|
19
|
+
| "warning"
|
|
20
|
+
| "danger"
|
|
21
|
+
// literals last so scheme-name autocomplete still surfaces
|
|
22
|
+
| (string & {});
|
|
23
|
+
|
|
24
|
+
export interface BadgeProps extends ViewProps {
|
|
25
|
+
/** The element being badged. Rendered untouched; `Badge` only adds the overlay. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Corner the indicator sits on. Defaults to `'top-right'`.
|
|
29
|
+
*
|
|
30
|
+
* The indicator overhangs the child, so an ancestor that clips — a `Surface`
|
|
31
|
+
* with a radius, or a `Screen` list row — will cut it off. Pad that container
|
|
32
|
+
* by the overhang, or lift the `Badge` outside it.
|
|
33
|
+
*/
|
|
34
|
+
placement?: BadgePlacement;
|
|
35
|
+
/**
|
|
36
|
+
* Text in the indicator. Omit it (or pass an empty string) for a dot.
|
|
37
|
+
* `0` renders as a label — use `isVisible` to suppress a zero count.
|
|
38
|
+
*/
|
|
39
|
+
label?: string | number;
|
|
40
|
+
/** Semantic scheme or raw color literal. Defaults to `'danger'`. */
|
|
41
|
+
color?: BadgeColor;
|
|
42
|
+
/** Scale of the indicator. Defaults to `'md'`. */
|
|
43
|
+
size?: BadgeSize;
|
|
44
|
+
/**
|
|
45
|
+
* Nudges the indicator along both axes from the corner `placement` picked.
|
|
46
|
+
* Positive moves it away from the child's centre, negative pulls it in.
|
|
47
|
+
* A number applies to both axes; `{ x, y }` sets them separately.
|
|
48
|
+
*
|
|
49
|
+
* The corner it anchors to is the child's **bounding box**, so a round child
|
|
50
|
+
* needs a negative offset to bring the indicator onto the visible edge — a
|
|
51
|
+
* circle only meets its box at 45°, about `0.21 × width` away. Defaults to `0`.
|
|
52
|
+
*/
|
|
53
|
+
offset?: number | { x?: number; y?: number };
|
|
54
|
+
/** Renders the indicator. The child renders either way. Defaults to `true`. */
|
|
55
|
+
isVisible?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Stretches the wrapper along its parent's cross axis, so a child that
|
|
58
|
+
* stretches unwrapped keeps stretching. Defaults to `false`, which hugs the
|
|
59
|
+
* child — what an icon, avatar, or toolbar button needs. An explicit
|
|
60
|
+
* `alignSelf` in `style` beats both.
|
|
61
|
+
*/
|
|
62
|
+
fullWidth?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Announced in place of the label. Defaults to the label's own text; an
|
|
65
|
+
* unlabelled dot is hidden from assistive technology.
|
|
66
|
+
*/
|
|
67
|
+
accessibilityLabel?: string;
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readableOn } from "#utils/color-utils";
|
|
2
|
+
import type { BadgePlacement, BadgeSize } from "./types";
|
|
3
|
+
|
|
4
|
+
// A custom color literal (hex/rgb/hsl) isn't a bounded set of options, so it
|
|
5
|
+
// can't be a unistyles variant condition — it's derived here instead, the same
|
|
6
|
+
// way `chip/utils.ts` handles the equivalent case on `Chip`.
|
|
7
|
+
|
|
8
|
+
export interface CustomBadgeColors {
|
|
9
|
+
bg: string;
|
|
10
|
+
fg: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The indicator's paint for a raw literal: the literal itself, plus a readable label. */
|
|
14
|
+
export function getCustomBadgeColors(literal: string): CustomBadgeColors {
|
|
15
|
+
return { bg: literal, fg: readableOn(literal) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `true` when `label` should render as a pill. An empty string and `undefined`
|
|
20
|
+
* are a dot; `0` is a label, since a caller suppressing a zero count uses
|
|
21
|
+
* `isVisible` rather than relying on falsiness.
|
|
22
|
+
*/
|
|
23
|
+
export function hasLabel(label: string | number | undefined): label is string | number {
|
|
24
|
+
return label !== undefined && label !== "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Outer diameter of the dot per size. The indicator's own stylesheet repeats
|
|
29
|
+
* these as literals — the unistyles Babel plugin reads that object statically,
|
|
30
|
+
* so it cannot reference this table — and `anchorOffsetsMatchDotRadius` in the
|
|
31
|
+
* test suite is what keeps the two in step.
|
|
32
|
+
*/
|
|
33
|
+
export const DOT_SIZE: Record<BadgeSize, number> = {
|
|
34
|
+
sm: 12,
|
|
35
|
+
md: 16,
|
|
36
|
+
lg: 20,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface AnchorPosition {
|
|
40
|
+
top?: number;
|
|
41
|
+
right?: number;
|
|
42
|
+
bottom?: number;
|
|
43
|
+
left?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Absolute insets for the indicator, in pure arithmetic rather than one
|
|
48
|
+
* compound variant per (placement, size) cell: `offset` is an open-ended number
|
|
49
|
+
* and could never be a variant condition, and nothing here touches the theme.
|
|
50
|
+
*
|
|
51
|
+
* The base inset is the dot's radius, which puts a dot's centre exactly on the
|
|
52
|
+
* child's bounding-box corner. `offset` moves it from there — positive away
|
|
53
|
+
* from the child's centre, negative toward it.
|
|
54
|
+
*/
|
|
55
|
+
export function getAnchorPosition(
|
|
56
|
+
placement: BadgePlacement,
|
|
57
|
+
size: BadgeSize,
|
|
58
|
+
offset: number | { x?: number; y?: number } = 0
|
|
59
|
+
): AnchorPosition {
|
|
60
|
+
const base = DOT_SIZE[size] / 2;
|
|
61
|
+
const { x, y } =
|
|
62
|
+
typeof offset === "number" ? { x: offset, y: offset } : { x: offset.x ?? 0, y: offset.y ?? 0 };
|
|
63
|
+
|
|
64
|
+
const insetX = -(base + x);
|
|
65
|
+
const insetY = -(base + y);
|
|
66
|
+
|
|
67
|
+
switch (placement) {
|
|
68
|
+
case "top-left":
|
|
69
|
+
return { top: insetY, left: insetX };
|
|
70
|
+
case "bottom-right":
|
|
71
|
+
return { bottom: insetY, right: insetX };
|
|
72
|
+
case "bottom-left":
|
|
73
|
+
return { bottom: insetY, left: insetX };
|
|
74
|
+
default:
|
|
75
|
+
return { top: insetY, right: insetX };
|
|
76
|
+
}
|
|
77
|
+
}
|