@krizaka/ui 3.44.0 → 3.46.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/native/vertical-video.tsx"],"sourcesContent":["// @krizaka/ui/native/vertical-video — its own entry (it plays video on expo-video, an optional peer). A 9:16 clip filling\n// its frame, the action rail on the right (avatar + follow, like, comments, share, a slot for the product's tip), the\n// creator's caption bottom-left, a progress hairline; `ShortFeed` pages them vertically and plays the one in view.\n// Over the picture everything reads on the theme's invariant `scrim` / `textOnMedia`.\nimport { formatCompact } from \"@krizaka/intl\";\nimport { Avatar, Txt, useTheme, VerifiedBadge } from \"@krizaka/ui/native\";\nimport { VideoPlayer } from \"@krizaka/ui/native/video-player\";\nimport * as React from \"react\";\nimport { FlatList, Image,Pressable, type StyleProp, StyleSheet, View, type ViewStyle, type ViewToken } from \"react-native\";\nimport Svg, { Path } from \"react-native-svg\";\n\nexport interface VerticalVideoLabels {\n play: string;\n pause: string;\n like: string;\n comment: string;\n share: string;\n more: string;\n /** \"Follow {name}\". */\n follow: string;\n /** \"Following {name}\". */\n following: string;\n expand: string;\n collapse: string;\n progress: string;\n /** The rail's name (\"Actions\"). */\n actions: string;\n}\n\nexport const verticalVideoLabels: VerticalVideoLabels = {\n play: \"Play\",\n pause: \"Pause\",\n like: \"Like\",\n comment: \"Comments\",\n share: \"Share\",\n more: \"More\",\n follow: \"Follow {name}\",\n following: \"Following {name}\",\n expand: \"More\",\n collapse: \"Less\",\n progress: \"Progress\",\n actions: \"Actions\",\n};\n\nexport type VerticalVideoProps = {\n /** The video's accessible name (\"Video: Night shoot, part 2\"), passed translated. */\n title: string;\n /** The clip's address (MP4 or HLS). Absent, the poster alone shows. */\n uri?: string;\n /** The picture until the first frame. */\n posterUri?: string;\n /** Plays while `true` (a `ShortFeed` passes it for the slide in view). Default `true`. */\n active?: boolean;\n /** Who made it: name, portrait, the seal's words. */\n creator: { name: string; avatarUri?: string; verifiedLabel?: string };\n /** A pill beside the name (\"AI-GENERATED\"). */\n disclosure?: string;\n /** The caption, two lines until expanded. */\n caption?: string;\n /** Tags after the caption (without `#`). */\n tags?: readonly string[];\n /** The sound or credit line. */\n credit?: string;\n /** The like count, compact in `locale`. */\n likes?: number;\n /** The comment count. */\n comments?: number;\n /** The share count. */\n shares?: number;\n /** Whether the viewer likes it. */\n liked?: boolean;\n /** Whether the viewer follows the creator. */\n following?: boolean;\n /** The viewer likes or unlikes. */\n onLike?: (liked: boolean) => void;\n /** Opens the comments. */\n onComment?: () => void;\n /** Opens the share sheet. */\n onShare?: () => void;\n /** Follows or unfollows; without it the avatar has no \"+\". */\n onFollow?: (following: boolean) => void;\n /** The \"more\" action; without it the button is not drawn. */\n onMore?: () => void;\n /** The product's own action on the rail (a tip, an unlock): a named, 44 pt button of yours. */\n tipSlot?: React.ReactNode;\n /** Top left over the picture: a chip (an auction's time left). */\n topStart?: React.ReactNode;\n /** Above the caption: a goal gauge. */\n aboveCaption?: React.ReactNode;\n /** The locale of the counts. Default `en`. */\n locale?: string;\n /** Every word, passed translated. */\n labels?: Partial<VerticalVideoLabels>;\n /** Styles merged last, on the frame. */\n style?: StyleProp<ViewStyle>;\n};\n\nconst HEART = \"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z\";\nconst COMMENT = \"M4 5h16v11H9l-5 4z\";\nconst SHARE = \"M4 12v7h16v-7M12 3v12M7 8l5-5 5 5\";\nconst MORE = \"M5 12h.01M12 12h.01M19 12h.01\";\n\nfunction Glyph({ d, color, filled }: { d: string; color: string; filled?: boolean }) {\n return (\n <Svg width={24} height={24} viewBox=\"0 0 24 24\" fill={filled ? color : \"none\"} stroke={color} strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <Path d={d} />\n </Svg>\n );\n}\n\nfunction RailButton({ label, count, pressed, onPress, children }: { label: string; count?: string | undefined; pressed?: boolean | undefined; onPress?: (() => void) | undefined; children: React.ReactNode }) {\n const { theme } = useTheme();\n return (\n <View style={styles.railItem}>\n <Pressable role=\"button\" aria-label={label} aria-pressed={pressed} onPress={onPress} hitSlop={4} style={[styles.railButton, { backgroundColor: theme.scrim }]}>\n {children}\n </Pressable>\n {count !== undefined ? (\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.count}>\n {count}\n </Txt>\n ) : null}\n </View>\n );\n}\n\n/** One short video: 9:16, the rail of actions on the right, the creator's caption bottom-left. */\nexport function VerticalVideo({\n title,\n uri,\n posterUri,\n active = true,\n creator,\n disclosure,\n caption,\n tags = [],\n credit,\n likes,\n comments,\n shares,\n liked = false,\n following = false,\n onLike,\n onComment,\n onShare,\n onFollow,\n onMore,\n tipSlot,\n topStart,\n aboveCaption,\n locale = \"en\",\n labels: custom,\n style,\n}: VerticalVideoProps) {\n const { theme } = useTheme();\n const labels = { ...verticalVideoLabels, ...custom };\n const [open, setOpen] = React.useState(false);\n const [paused, setPaused] = React.useState(false);\n const count = (n?: number) => (n === undefined ? undefined : formatCompact(n, { locale }));\n const on = theme.textOnMedia;\n return (\n <View role=\"group\" aria-label={title} style={[styles.frame, { backgroundColor: theme.media }, style]}>\n {uri ? (\n <VideoPlayer source={{ uri }} title={title} aspectRatio={9 / 16} contentFit=\"cover\" nativeControls={false} fullscreen={false} autoPlay loop muted paused={!active || paused} style={StyleSheet.absoluteFill} {...(posterUri ? { poster: { uri: posterUri } } : {})} />\n ) : posterUri ? (\n <Image source={{ uri: posterUri }} aria-hidden resizeMode=\"cover\" style={StyleSheet.absoluteFill} />\n ) : null}\n <Pressable role=\"button\" aria-label={paused ? labels.play : labels.pause} onPress={() => setPaused((p) => !p)} onLongPress={() => onLike?.(!liked)} style={StyleSheet.absoluteFill} />\n <View pointerEvents=\"none\" style={[styles.shade, { backgroundColor: theme.scrim }]} />\n {topStart ? <View style={styles.topStart}>{topStart}</View> : null}\n\n <View role=\"group\" aria-label={labels.actions} style={styles.rail}>\n <View>\n <Avatar alt={creator.name} {...(creator.avatarUri ? { src: creator.avatarUri } : {})} size=\"md\" />\n {onFollow ? (\n <Pressable\n role=\"button\"\n aria-label={(following ? labels.following : labels.follow).replace(\"{name}\", creator.name)}\n aria-pressed={following}\n onPress={() => onFollow(!following)}\n hitSlop={12}\n style={[styles.follow, { backgroundColor: following ? theme.surface2 : theme.accent }]}\n >\n <Glyph d={following ? \"M5 12l5 5 9-10\" : \"M12 5v14M5 12h14\"} color={following ? theme.textPrimary : theme.onAccent} />\n </Pressable>\n ) : null}\n </View>\n <RailButton label={labels.like} count={count(likes)} pressed={liked} onPress={() => onLike?.(!liked)}>\n <Glyph d={HEART} color={liked ? theme.accent : on} filled={liked} />\n </RailButton>\n <RailButton label={labels.comment} count={count(comments)} onPress={onComment}>\n <Glyph d={COMMENT} color={on} />\n </RailButton>\n <RailButton label={labels.share} count={count(shares)} onPress={onShare}>\n <Glyph d={SHARE} color={on} />\n </RailButton>\n {tipSlot}\n {onMore ? (\n <RailButton label={labels.more} onPress={onMore}>\n <Glyph d={MORE} color={on} />\n </RailButton>\n ) : null}\n </View>\n\n <View style={styles.caption}>\n {aboveCaption}\n <View style={styles.nameRow}>\n <Txt variant=\"label\" tone=\"onMedia\">\n {creator.name}\n </Txt>\n {creator.verifiedLabel ? <VerifiedBadge label={creator.verifiedLabel} on=\"media\" size={14} /> : null}\n {disclosure ? (\n <View style={[styles.pill, { backgroundColor: theme.scrim }]}>\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.pillText}>\n {disclosure.toUpperCase()}\n </Txt>\n </View>\n ) : null}\n </View>\n {caption ? (\n <Txt tone=\"onMedia\" numberOfLines={open ? undefined : 2}>\n {caption}\n {tags.length > 0 ? ` ${tags.map((tag) => `#${tag}`).join(\" \")}` : \"\"}\n </Txt>\n ) : null}\n {caption && caption.length > 90 ? (\n <Pressable role=\"button\" aria-label={open ? labels.collapse : labels.expand} aria-expanded={open} onPress={() => setOpen((v) => !v)} hitSlop={10}>\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.underline}>\n {open ? labels.collapse : labels.expand}\n </Txt>\n </Pressable>\n ) : null}\n {credit ? (\n <Txt variant=\"caption\" tone=\"onMedia\">\n {credit}\n </Txt>\n ) : null}\n </View>\n </View>\n );\n}\n\nexport type ShortFeedProps<T> = {\n /** The feed's accessible name (\"Videos\"). */\n label: string;\n /** The videos, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One slide: `active` is true for the one in view. */\n renderItem: (item: T, state: { active: boolean; index: number }) => React.ReactElement;\n /** The page's height in points (the feed's own height; one slide per page). */\n height: number;\n /** Called when the slide in view changes. */\n onActiveChange?: (index: number, item: T) => void;\n /** Styles merged last. */\n style?: StyleProp<ViewStyle>;\n};\n\n/** A vertical paging feed: one video per screen, the one in view plays, the others pause. */\nexport function ShortFeed<T>({ label, items, getId, renderItem, height, onActiveChange, style }: ShortFeedProps<T>) {\n const [active, setActive] = React.useState(0);\n // FlatList wants one stable callback for its whole life.\n const [onViewable] = React.useState(() => ({ viewableItems }: { viewableItems: ViewToken[] }) => {\n const first = viewableItems.find((v) => v.isViewable && typeof v.index === \"number\");\n if (first && typeof first.index === \"number\") setActive(first.index);\n });\n React.useEffect(() => {\n const item = items[active];\n if (item !== undefined) onActiveChange?.(active, item);\n // Only a change of the slide in view is an event.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active]);\n return (\n <View role=\"list\" aria-label={label} style={[{ height }, style]}>\n <FlatList\n data={items as T[]}\n keyExtractor={getId}\n pagingEnabled\n showsVerticalScrollIndicator={false}\n snapToInterval={height}\n decelerationRate=\"fast\"\n getItemLayout={(_, index) => ({ length: height, offset: height * index, index })}\n windowSize={3}\n initialNumToRender={2}\n viewabilityConfig={{ itemVisiblePercentThreshold: 60 }}\n onViewableItemsChanged={onViewable}\n extraData={active}\n style={{ height }}\n renderItem={({ item, index }) => <View style={{ height }}>{renderItem(item, { active: index === active, index })}</View>}\n />\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n frame: { width: \"100%\", aspectRatio: 9 / 16, overflow: \"hidden\" },\n shade: { position: \"absolute\", left: 0, right: 0, bottom: 0, height: \"30%\", opacity: 0.55 },\n topStart: { position: \"absolute\", left: 12, top: 12 },\n rail: { position: \"absolute\", right: 8, bottom: 20, alignItems: \"center\", gap: 12 },\n railItem: { alignItems: \"center\", gap: 2 },\n railButton: { width: 44, height: 44, borderRadius: 22, alignItems: \"center\", justifyContent: \"center\" },\n count: { fontVariant: [\"lining-nums\", \"tabular-nums\"], fontWeight: \"600\" },\n follow: { position: \"absolute\", bottom: -10, alignSelf: \"center\", width: 22, height: 22, borderRadius: 11, alignItems: \"center\", justifyContent: \"center\", overflow: \"hidden\" },\n caption: { position: \"absolute\", left: 12, right: 76, bottom: 16, gap: 6 },\n nameRow: { flexDirection: \"row\", alignItems: \"center\", gap: 6 },\n pill: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999 },\n pillText: { fontWeight: \"700\", letterSpacing: 0.4 },\n underline: { textDecorationLine: \"underline\", fontWeight: \"600\" },\n});\n"],"mappings":";AAIA,SAAS,qBAAqB;AAC9B,SAAS,QAAQ,KAAK,UAAU,qBAAqB;AACrD,SAAS,mBAAmB;AAC5B,YAAY,WAAW;AACvB,SAAS,UAAU,OAAM,WAA2B,YAAY,YAA4C;AAC5G,OAAO,OAAO,YAAY;AAgGpB,cAQF,YARE;AA5EC,IAAM,sBAA2C;AAAA,EACtD,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,UAAU;AAAA,EACV,SAAS;AACX;AAuDA,IAAM,QAAQ;AACd,IAAM,UAAU;AAChB,IAAM,QAAQ;AACd,IAAM,OAAO;AAEb,SAAS,MAAM,EAAE,GAAG,OAAO,OAAO,GAAmD;AACnF,SACE,oBAAC,OAAI,OAAO,IAAI,QAAQ,IAAI,SAAQ,aAAY,MAAM,SAAS,QAAQ,QAAQ,QAAQ,OAAO,aAAa,GAAG,eAAc,SAAQ,gBAAe,SAAQ,eAAW,MACpK,8BAAC,QAAK,GAAM,GACd;AAEJ;AAEA,SAAS,WAAW,EAAE,OAAO,OAAO,SAAS,SAAS,SAAS,GAAgJ;AAC7M,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,SACE,qBAAC,QAAK,OAAO,OAAO,UAClB;AAAA,wBAAC,aAAU,MAAK,UAAS,cAAY,OAAO,gBAAc,SAAS,SAAkB,SAAS,GAAG,OAAO,CAAC,OAAO,YAAY,EAAE,iBAAiB,MAAM,MAAM,CAAC,GACzJ,UACH;AAAA,IACC,UAAU,SACT,oBAAC,OAAI,SAAQ,WAAU,MAAK,WAAU,OAAO,OAAO,OACjD,iBACH,IACE;AAAA,KACN;AAEJ;AAGO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO,CAAC;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AACF,GAAuB;AACrB,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,SAAS,EAAE,GAAG,qBAAqB,GAAG,OAAO;AACnD,QAAM,CAAC,MAAM,OAAO,IAAU,eAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAS,KAAK;AAChD,QAAM,QAAQ,CAAC,MAAgB,MAAM,SAAY,SAAY,cAAc,GAAG,EAAE,OAAO,CAAC;AACxF,QAAM,KAAK,MAAM;AACjB,SACE,qBAAC,QAAK,MAAK,SAAQ,cAAY,OAAO,OAAO,CAAC,OAAO,OAAO,EAAE,iBAAiB,MAAM,MAAM,GAAG,KAAK,GAChG;AAAA,UACC,oBAAC,eAAY,QAAQ,EAAE,IAAI,GAAG,OAAc,aAAa,IAAI,IAAI,YAAW,SAAQ,gBAAgB,OAAO,YAAY,OAAO,UAAQ,MAAC,MAAI,MAAC,OAAK,MAAC,QAAQ,CAAC,UAAU,QAAQ,OAAO,WAAW,cAAe,GAAI,YAAY,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,IAAI,CAAC,GAAI,IAClQ,YACF,oBAAC,SAAM,QAAQ,EAAE,KAAK,UAAU,GAAG,eAAW,MAAC,YAAW,SAAQ,OAAO,WAAW,cAAc,IAChG;AAAA,IACJ,oBAAC,aAAU,MAAK,UAAS,cAAY,SAAS,OAAO,OAAO,OAAO,OAAO,SAAS,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,aAAa,MAAM,SAAS,CAAC,KAAK,GAAG,OAAO,WAAW,cAAc;AAAA,IACpL,oBAAC,QAAK,eAAc,QAAO,OAAO,CAAC,OAAO,OAAO,EAAE,iBAAiB,MAAM,MAAM,CAAC,GAAG;AAAA,IACnF,WAAW,oBAAC,QAAK,OAAO,OAAO,UAAW,oBAAS,IAAU;AAAA,IAE9D,qBAAC,QAAK,MAAK,SAAQ,cAAY,OAAO,SAAS,OAAO,OAAO,MAC3D;AAAA,2BAAC,QACC;AAAA,4BAAC,UAAO,KAAK,QAAQ,MAAO,GAAI,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,IAAI,CAAC,GAAI,MAAK,MAAK;AAAA,QAC/F,WACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,eAAa,YAAY,OAAO,YAAY,OAAO,QAAQ,QAAQ,UAAU,QAAQ,IAAI;AAAA,YACzF,gBAAc;AAAA,YACd,SAAS,MAAM,SAAS,CAAC,SAAS;AAAA,YAClC,SAAS;AAAA,YACT,OAAO,CAAC,OAAO,QAAQ,EAAE,iBAAiB,YAAY,MAAM,WAAW,MAAM,OAAO,CAAC;AAAA,YAErF,8BAAC,SAAM,GAAG,YAAY,mBAAmB,oBAAoB,OAAO,YAAY,MAAM,cAAc,MAAM,UAAU;AAAA;AAAA,QACtH,IACE;AAAA,SACN;AAAA,MACA,oBAAC,cAAW,OAAO,OAAO,MAAM,OAAO,MAAM,KAAK,GAAG,SAAS,OAAO,SAAS,MAAM,SAAS,CAAC,KAAK,GACjG,8BAAC,SAAM,GAAG,OAAO,OAAO,QAAQ,MAAM,SAAS,IAAI,QAAQ,OAAO,GACpE;AAAA,MACA,oBAAC,cAAW,OAAO,OAAO,SAAS,OAAO,MAAM,QAAQ,GAAG,SAAS,WAClE,8BAAC,SAAM,GAAG,SAAS,OAAO,IAAI,GAChC;AAAA,MACA,oBAAC,cAAW,OAAO,OAAO,OAAO,OAAO,MAAM,MAAM,GAAG,SAAS,SAC9D,8BAAC,SAAM,GAAG,OAAO,OAAO,IAAI,GAC9B;AAAA,MACC;AAAA,MACA,SACC,oBAAC,cAAW,OAAO,OAAO,MAAM,SAAS,QACvC,8BAAC,SAAM,GAAG,MAAM,OAAO,IAAI,GAC7B,IACE;AAAA,OACN;AAAA,IAEA,qBAAC,QAAK,OAAO,OAAO,SACjB;AAAA;AAAA,MACD,qBAAC,QAAK,OAAO,OAAO,SAClB;AAAA,4BAAC,OAAI,SAAQ,SAAQ,MAAK,WACvB,kBAAQ,MACX;AAAA,QACC,QAAQ,gBAAgB,oBAAC,iBAAc,OAAO,QAAQ,eAAe,IAAG,SAAQ,MAAM,IAAI,IAAK;AAAA,QAC/F,aACC,oBAAC,QAAK,OAAO,CAAC,OAAO,MAAM,EAAE,iBAAiB,MAAM,MAAM,CAAC,GACzD,8BAAC,OAAI,SAAQ,WAAU,MAAK,WAAU,OAAO,OAAO,UACjD,qBAAW,YAAY,GAC1B,GACF,IACE;AAAA,SACN;AAAA,MACC,UACC,qBAAC,OAAI,MAAK,WAAU,eAAe,OAAO,SAAY,GACnD;AAAA;AAAA,QACA,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC,KAAK;AAAA,SACpE,IACE;AAAA,MACH,WAAW,QAAQ,SAAS,KAC3B,oBAAC,aAAU,MAAK,UAAS,cAAY,OAAO,OAAO,WAAW,OAAO,QAAQ,iBAAe,MAAM,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,IAC5I,8BAAC,OAAI,SAAQ,WAAU,MAAK,WAAU,OAAO,OAAO,WACjD,iBAAO,OAAO,WAAW,OAAO,QACnC,GACF,IACE;AAAA,MACH,SACC,oBAAC,OAAI,SAAQ,WAAU,MAAK,WACzB,kBACH,IACE;AAAA,OACN;AAAA,KACF;AAEJ;AAoBO,SAAS,UAAa,EAAE,OAAO,OAAO,OAAO,YAAY,QAAQ,gBAAgB,MAAM,GAAsB;AAClH,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAS,CAAC;AAE5C,QAAM,CAAC,UAAU,IAAU,eAAS,MAAM,CAAC,EAAE,cAAc,MAAsC;AAC/F,UAAM,QAAQ,cAAc,KAAK,CAAC,MAAM,EAAE,cAAc,OAAO,EAAE,UAAU,QAAQ;AACnF,QAAI,SAAS,OAAO,MAAM,UAAU,SAAU,WAAU,MAAM,KAAK;AAAA,EACrE,CAAC;AACD,EAAM,gBAAU,MAAM;AACpB,UAAM,OAAO,MAAM,MAAM;AACzB,QAAI,SAAS,OAAW,kBAAiB,QAAQ,IAAI;AAAA,EAGvD,GAAG,CAAC,MAAM,CAAC;AACX,SACE,oBAAC,QAAK,MAAK,QAAO,cAAY,OAAO,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,GAC9D;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN,cAAc;AAAA,MACd,eAAa;AAAA,MACb,8BAA8B;AAAA,MAC9B,gBAAgB;AAAA,MAChB,kBAAiB;AAAA,MACjB,eAAe,CAAC,GAAG,WAAW,EAAE,QAAQ,QAAQ,QAAQ,SAAS,OAAO,MAAM;AAAA,MAC9E,YAAY;AAAA,MACZ,oBAAoB;AAAA,MACpB,mBAAmB,EAAE,6BAA6B,GAAG;AAAA,MACrD,wBAAwB;AAAA,MACxB,WAAW;AAAA,MACX,OAAO,EAAE,OAAO;AAAA,MAChB,YAAY,CAAC,EAAE,MAAM,MAAM,MAAM,oBAAC,QAAK,OAAO,EAAE,OAAO,GAAI,qBAAW,MAAM,EAAE,QAAQ,UAAU,QAAQ,MAAM,CAAC,GAAE;AAAA;AAAA,EACnH,GACA;AAEJ;AAEA,IAAM,SAAS,WAAW,OAAO;AAAA,EAC/B,OAAO,EAAE,OAAO,QAAQ,aAAa,IAAI,IAAI,UAAU,SAAS;AAAA,EAChE,OAAO,EAAE,UAAU,YAAY,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,OAAO,SAAS,KAAK;AAAA,EAC1F,UAAU,EAAE,UAAU,YAAY,MAAM,IAAI,KAAK,GAAG;AAAA,EACpD,MAAM,EAAE,UAAU,YAAY,OAAO,GAAG,QAAQ,IAAI,YAAY,UAAU,KAAK,GAAG;AAAA,EAClF,UAAU,EAAE,YAAY,UAAU,KAAK,EAAE;AAAA,EACzC,YAAY,EAAE,OAAO,IAAI,QAAQ,IAAI,cAAc,IAAI,YAAY,UAAU,gBAAgB,SAAS;AAAA,EACtG,OAAO,EAAE,aAAa,CAAC,eAAe,cAAc,GAAG,YAAY,MAAM;AAAA,EACzE,QAAQ,EAAE,UAAU,YAAY,QAAQ,KAAK,WAAW,UAAU,OAAO,IAAI,QAAQ,IAAI,cAAc,IAAI,YAAY,UAAU,gBAAgB,UAAU,UAAU,SAAS;AAAA,EAC9K,SAAS,EAAE,UAAU,YAAY,MAAM,IAAI,OAAO,IAAI,QAAQ,IAAI,KAAK,EAAE;AAAA,EACzE,SAAS,EAAE,eAAe,OAAO,YAAY,UAAU,KAAK,EAAE;AAAA,EAC9D,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,cAAc,IAAI;AAAA,EACpE,UAAU,EAAE,YAAY,OAAO,eAAe,IAAI;AAAA,EAClD,WAAW,EAAE,oBAAoB,aAAa,YAAY,MAAM;AAClE,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/native/vertical-video.tsx","../../src/vertical-video/model.ts"],"sourcesContent":["// @krizaka/ui/native/vertical-video — its own entry (it plays video on expo-video, an optional peer). A 9:16 clip filling\n// its frame, the action rail on the right (avatar + follow, like, comments, share, a slot for the product's tip), the\n// creator's caption bottom-left, a progress hairline; `ShortFeed` pages them vertically and plays the one in view.\n// Over the picture everything reads on the theme's invariant `scrim` / `textOnMedia`.\nimport { formatCompact } from \"@krizaka/intl\";\nimport { Avatar, Txt, useTheme, VerifiedBadge } from \"@krizaka/ui/native\";\nimport { VideoPlayer } from \"@krizaka/ui/native/video-player\";\nimport * as React from \"react\";\nimport { FlatList, Image,Pressable, type StyleProp, StyleSheet, View, type ViewStyle, type ViewToken } from \"react-native\";\nimport Svg, { Path } from \"react-native-svg\";\n\nimport { type VerticalVideoLabels, verticalVideoLabels, type VerticalVideoShared } from \"../vertical-video/model\";\n\nexport { type VerticalVideoLabels, verticalVideoLabels };\n\nexport type VerticalVideoProps = VerticalVideoShared & {\n /** The clip's address (MP4 or HLS). Absent, the poster alone shows. */\n uri?: string;\n /** The picture until the first frame. */\n posterUri?: string;\n /** Who made it: name, portrait, the seal's words. */\n creator: { name: string; avatarUri?: string; verifiedLabel?: string };\n /** Styles merged last, on the frame. */\n style?: StyleProp<ViewStyle>;\n};\n\nconst HEART = \"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z\";\nconst COMMENT = \"M4 5h16v11H9l-5 4z\";\nconst SHARE = \"M4 12v7h16v-7M12 3v12M7 8l5-5 5 5\";\nconst MORE = \"M5 12h.01M12 12h.01M19 12h.01\";\n\nfunction Glyph({ d, color, filled }: { d: string; color: string; filled?: boolean }) {\n return (\n <Svg width={24} height={24} viewBox=\"0 0 24 24\" fill={filled ? color : \"none\"} stroke={color} strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <Path d={d} />\n </Svg>\n );\n}\n\nfunction RailButton({ label, count, pressed, onPress, children }: { label: string; count?: string | undefined; pressed?: boolean | undefined; onPress?: (() => void) | undefined; children: React.ReactNode }) {\n const { theme } = useTheme();\n return (\n <View style={styles.railItem}>\n <Pressable role=\"button\" aria-label={label} aria-pressed={pressed} onPress={onPress} hitSlop={4} style={[styles.railButton, { backgroundColor: theme.scrim }]}>\n {children}\n </Pressable>\n {count !== undefined ? (\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.count}>\n {count}\n </Txt>\n ) : null}\n </View>\n );\n}\n\n/** One short video: 9:16, the rail of actions on the right, the creator's caption bottom-left. */\nexport function VerticalVideo({\n title,\n uri,\n posterUri,\n active = true,\n creator,\n disclosure,\n caption,\n tags = [],\n credit,\n likes,\n comments,\n shares,\n liked = false,\n following = false,\n onLike,\n onComment,\n onShare,\n onFollow,\n onMore,\n tipSlot,\n topStart,\n aboveCaption,\n locale = \"en\",\n labels: custom,\n style,\n}: VerticalVideoProps) {\n const { theme } = useTheme();\n const labels = { ...verticalVideoLabels, ...custom };\n const [open, setOpen] = React.useState(false);\n const [paused, setPaused] = React.useState(false);\n const count = (n?: number) => (n === undefined ? undefined : formatCompact(n, { locale }));\n const on = theme.textOnMedia;\n return (\n <View role=\"group\" aria-label={title} style={[styles.frame, { backgroundColor: theme.media }, style]}>\n {uri ? (\n <VideoPlayer source={{ uri }} title={title} aspectRatio={9 / 16} contentFit=\"cover\" nativeControls={false} fullscreen={false} autoPlay loop muted paused={!active || paused} style={StyleSheet.absoluteFill} {...(posterUri ? { poster: { uri: posterUri } } : {})} />\n ) : posterUri ? (\n <Image source={{ uri: posterUri }} aria-hidden resizeMode=\"cover\" style={StyleSheet.absoluteFill} />\n ) : null}\n <Pressable role=\"button\" aria-label={paused ? labels.play : labels.pause} onPress={() => setPaused((p) => !p)} onLongPress={() => onLike?.(!liked)} style={StyleSheet.absoluteFill} />\n <View pointerEvents=\"none\" style={[styles.shade, { backgroundColor: theme.scrim }]} />\n {topStart ? <View style={styles.topStart}>{topStart}</View> : null}\n\n <View role=\"group\" aria-label={labels.actions} style={styles.rail}>\n <View>\n <Avatar alt={creator.name} {...(creator.avatarUri ? { src: creator.avatarUri } : {})} size=\"md\" />\n {onFollow ? (\n <Pressable\n role=\"button\"\n aria-label={(following ? labels.following : labels.follow).replace(\"{name}\", creator.name)}\n aria-pressed={following}\n onPress={() => onFollow(!following)}\n hitSlop={12}\n style={[styles.follow, { backgroundColor: following ? theme.surface2 : theme.accent }]}\n >\n <Glyph d={following ? \"M5 12l5 5 9-10\" : \"M12 5v14M5 12h14\"} color={following ? theme.textPrimary : theme.onAccent} />\n </Pressable>\n ) : null}\n </View>\n <RailButton label={labels.like} count={count(likes)} pressed={liked} onPress={() => onLike?.(!liked)}>\n <Glyph d={HEART} color={liked ? theme.accent : on} filled={liked} />\n </RailButton>\n <RailButton label={labels.comment} count={count(comments)} onPress={onComment}>\n <Glyph d={COMMENT} color={on} />\n </RailButton>\n <RailButton label={labels.share} count={count(shares)} onPress={onShare}>\n <Glyph d={SHARE} color={on} />\n </RailButton>\n {tipSlot}\n {onMore ? (\n <RailButton label={labels.more} onPress={onMore}>\n <Glyph d={MORE} color={on} />\n </RailButton>\n ) : null}\n </View>\n\n <View style={styles.caption}>\n {aboveCaption}\n <View style={styles.nameRow}>\n <Txt variant=\"label\" tone=\"onMedia\">\n {creator.name}\n </Txt>\n {creator.verifiedLabel ? <VerifiedBadge label={creator.verifiedLabel} on=\"media\" size={14} /> : null}\n {disclosure ? (\n <View style={[styles.pill, { backgroundColor: theme.scrim }]}>\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.pillText}>\n {disclosure.toUpperCase()}\n </Txt>\n </View>\n ) : null}\n </View>\n {caption ? (\n <Txt tone=\"onMedia\" numberOfLines={open ? undefined : 2}>\n {caption}\n {tags.length > 0 ? ` ${tags.map((tag) => `#${tag}`).join(\" \")}` : \"\"}\n </Txt>\n ) : null}\n {caption && caption.length > 90 ? (\n <Pressable role=\"button\" aria-label={open ? labels.collapse : labels.expand} aria-expanded={open} onPress={() => setOpen((v) => !v)} hitSlop={10}>\n <Txt variant=\"caption\" tone=\"onMedia\" style={styles.underline}>\n {open ? labels.collapse : labels.expand}\n </Txt>\n </Pressable>\n ) : null}\n {credit ? (\n <Txt variant=\"caption\" tone=\"onMedia\">\n {credit}\n </Txt>\n ) : null}\n </View>\n </View>\n );\n}\n\nexport type ShortFeedProps<T> = {\n /** The feed's accessible name (\"Videos\"). */\n label: string;\n /** The videos, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One slide: `active` is true for the one in view. */\n renderItem: (item: T, state: { active: boolean; index: number }) => React.ReactElement;\n /** The page's height in points (the feed's own height; one slide per page). */\n height: number;\n /** Called when the slide in view changes. */\n onActiveChange?: (index: number, item: T) => void;\n /** Styles merged last. */\n style?: StyleProp<ViewStyle>;\n};\n\n/** A vertical paging feed: one video per screen, the one in view plays, the others pause. */\nexport function ShortFeed<T>({ label, items, getId, renderItem, height, onActiveChange, style }: ShortFeedProps<T>) {\n const [active, setActive] = React.useState(0);\n // FlatList wants one stable callback for its whole life.\n const [onViewable] = React.useState(() => ({ viewableItems }: { viewableItems: ViewToken[] }) => {\n const first = viewableItems.find((v) => v.isViewable && typeof v.index === \"number\");\n if (first && typeof first.index === \"number\") setActive(first.index);\n });\n React.useEffect(() => {\n const item = items[active];\n if (item !== undefined) onActiveChange?.(active, item);\n // Only a change of the slide in view is an event.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active]);\n return (\n <View role=\"list\" aria-label={label} style={[{ height }, style]}>\n <FlatList\n data={items as T[]}\n keyExtractor={getId}\n pagingEnabled\n showsVerticalScrollIndicator={false}\n snapToInterval={height}\n decelerationRate=\"fast\"\n getItemLayout={(_, index) => ({ length: height, offset: height * index, index })}\n windowSize={3}\n initialNumToRender={2}\n viewabilityConfig={{ itemVisiblePercentThreshold: 60 }}\n onViewableItemsChanged={onViewable}\n extraData={active}\n style={{ height }}\n renderItem={({ item, index }) => <View style={{ height }}>{renderItem(item, { active: index === active, index })}</View>}\n />\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n frame: { width: \"100%\", aspectRatio: 9 / 16, overflow: \"hidden\" },\n shade: { position: \"absolute\", left: 0, right: 0, bottom: 0, height: \"30%\", opacity: 0.55 },\n topStart: { position: \"absolute\", left: 12, top: 12 },\n rail: { position: \"absolute\", right: 8, bottom: 20, alignItems: \"center\", gap: 12 },\n railItem: { alignItems: \"center\", gap: 2 },\n railButton: { width: 44, height: 44, borderRadius: 22, alignItems: \"center\", justifyContent: \"center\" },\n count: { fontVariant: [\"lining-nums\", \"tabular-nums\"], fontWeight: \"600\" },\n follow: { position: \"absolute\", bottom: -10, alignSelf: \"center\", width: 22, height: 22, borderRadius: 11, alignItems: \"center\", justifyContent: \"center\", overflow: \"hidden\" },\n caption: { position: \"absolute\", left: 12, right: 76, bottom: 16, gap: 6 },\n nameRow: { flexDirection: \"row\", alignItems: \"center\", gap: 6 },\n pill: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999 },\n pillText: { fontWeight: \"700\", letterSpacing: 0.4 },\n underline: { textDecorationLine: \"underline\", fontWeight: \"600\" },\n});\n","// Headless: what the web (`vertical-video.tsx`) and the native (`native/vertical-video.tsx`) VerticalVideo share — the words,\n// the content and the callbacks. Each platform adds its media (`src`/`poster` · `uri`/`posterUri`) and its frame.\nimport type * as React from \"react\";\n\nexport interface VerticalVideoLabels {\n /** \"Play\" / \"Pause\" — the tap target over the picture. */\n play: string;\n pause: string;\n /** \"Like\" / \"Unlike\" (the button's name; the state is `aria-pressed`). */\n like: string;\n comment: string;\n share: string;\n more: string;\n /** \"Follow Elena\" — `{name}` is replaced by the creator's name. */\n follow: string;\n /** \"Following Elena\". */\n following: string;\n /** \"More\" / \"Less\" on a long caption. */\n expand: string;\n collapse: string;\n /** The progress bar's name (\"Progress\"). */\n progress: string;\n /** The rail's name (\"Actions\"). */\n actions: string;\n}\n\nexport const verticalVideoLabels: VerticalVideoLabels = {\n play: \"Play\",\n pause: \"Pause\",\n like: \"Like\",\n comment: \"Comments\",\n share: \"Share\",\n more: \"More\",\n follow: \"Follow {name}\",\n following: \"Following {name}\",\n expand: \"More\",\n collapse: \"Less\",\n progress: \"Progress\",\n actions: \"Actions\",\n};\n\n/** The props both platforms read: the same content, counts, state, callbacks and slots. */\nexport interface VerticalVideoShared {\n /** The video's accessible name (\"Video: Night shoot, part 2\"), passed translated. */\n title: string;\n /** Plays while `true`; a `ShortFeed` passes it for the slide in view. Default `true`. */\n active?: boolean;\n /** A pill beside the name: \"AI-GENERATED\" — said on the picture, never only in a page's footnote. */\n disclosure?: string;\n /** The caption: collapsed to two lines, expandable when long. */\n caption?: string;\n /** Tags under the caption (without `#`). */\n tags?: readonly string[];\n /** The sound or credit line (\"Original sound — Elena\"). */\n credit?: string;\n /** The like count under its button, compact (`12.4K`) in `locale`. */\n likes?: number;\n /** The comment count under its button. */\n comments?: number;\n /** The share count under its button. */\n shares?: number;\n /** Whether the viewer likes it: `aria-pressed` on the like button. */\n liked?: boolean;\n /** Whether the viewer follows the creator: the \"+\" on the avatar becomes a check. */\n following?: boolean;\n /** The viewer likes or unlikes (the button, `L`, a double tap). */\n onLike?: (liked: boolean) => void;\n /** Opens the comments; without it the button still shows. */\n onComment?: () => void;\n /** Opens the share sheet. */\n onShare?: () => void;\n /** The viewer follows or unfollows; without it the avatar has no \"+\". */\n onFollow?: (following: boolean) => void;\n /** The \"more\" button's action (report, hide); without it the button is not drawn. */\n onMore?: () => void;\n /** The product's own action on the rail: a tip, an unlock — a button of yours (≥ 44 px, named). */\n tipSlot?: React.ReactNode;\n /** Top left: a chip over the picture (an auction's time left). */\n topStart?: React.ReactNode;\n /** Above the caption: a goal gauge, a challenge's progress. */\n aboveCaption?: React.ReactNode;\n /** The locale of the counts. Default `en`. */\n locale?: string;\n /** Every word, passed translated. */\n labels?: Partial<VerticalVideoLabels>;\n}\n"],"mappings":"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","names":[]}
package/dist/native.d.ts CHANGED
@@ -1278,14 +1278,19 @@ type MetaRowProps = ViewProps & {
1278
1278
  /** The web `MetaRow`: one quiet line of dot-separated facts, with optional status chips at its end. */
1279
1279
  declare function MetaRow({ items, chips, label, style, ...props }: MetaRowProps): React$1.JSX.Element;
1280
1280
 
1281
- type MeterPart = {
1281
+ interface MeterPart {
1282
1282
  /** Its name (“To the creator”) — passed translated. */
1283
1283
  label: string;
1284
1284
  /** Its value, in the meter's unit. */
1285
1285
  value: number;
1286
- /** Its value in words (“€9.00”); default: its percentage. */
1286
+ /** Its value in words (“€9.00”), in the legend and for assistive technology; default: its percentage. */
1287
1287
  valueText?: string;
1288
- };
1288
+ }
1289
+ interface MeterThresholds {
1290
+ readonly low: number;
1291
+ readonly high: number;
1292
+ }
1293
+
1289
1294
  type MeterProps = {
1290
1295
  /** The accessible name (“Storage”, “Credits”) — passed translated; shown with `showLabel`. */
1291
1296
  label: string;
@@ -1298,10 +1303,7 @@ type MeterProps = {
1298
1303
  /** The value in words (“42 GB of 50 GB”); default: the percentage. */
1299
1304
  valueText?: string;
1300
1305
  /** The shares where the tone changes: below `low` good, up to `high` warning, past it the other end. */
1301
- thresholds?: {
1302
- readonly low: number;
1303
- readonly high: number;
1304
- };
1306
+ thresholds?: MeterThresholds;
1305
1307
  /** Which end is good: `low` (default: a quota) or `high` (a score). */
1306
1308
  optimum?: "low" | "high";
1307
1309
  /** Split into parts that add up to `max`: a stacked bar or ring, with its legend. */
package/dist/native.js CHANGED
@@ -938,12 +938,12 @@ function classesFor(config, props) {
938
938
  var empty = () => ({ frame: {}, text: {}, disabled: { frame: {}, text: {} }, on: { frame: {}, text: {} }, ignored: [] });
939
939
  var unit = (value) => /^\d+(\.\d+)?$/.test(value) ? Number(value) * 4 : void 0;
940
940
  function colour(name, theme) {
941
- const [role, percent] = name.split("/");
941
+ const [role, percent2] = name.split("/");
942
942
  if (role === "transparent") return "transparent";
943
943
  if (!role || !(role in ROLES)) return void 0;
944
944
  const resolved = theme[ROLES[role]];
945
945
  if (resolved === void 0) return void 0;
946
- return percent === void 0 ? resolved : withAlpha(resolved, Number(percent) / 100);
946
+ return percent2 === void 0 ? resolved : withAlpha(resolved, Number(percent2) / 100);
947
947
  }
948
948
  function apply(look3, cls, theme) {
949
949
  let target = look3;
@@ -3950,18 +3950,43 @@ function MetaRow({ items: items2, chips = [], label, style, ...props }) {
3950
3950
  // src/native/meter.tsx
3951
3951
  import { StyleSheet as StyleSheet29, View as View36 } from "react-native";
3952
3952
  import Svg19, { Circle as Circle12 } from "react-native-svg";
3953
+
3954
+ // src/meter/model.ts
3955
+ var MeterOptimum = { Low: "low", High: "high" };
3956
+ var MeterLevel = { Ok: "ok", Success: "success", Warning: "warning", Danger: "danger" };
3957
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
3958
+ var percent = (ratio) => `${Math.round(ratio * 100)} %`;
3959
+ function levelOf(ratio, thresholds, optimum) {
3960
+ if (!thresholds) return MeterLevel.Ok;
3961
+ const good = optimum === MeterOptimum.Low ? MeterLevel.Success : MeterLevel.Danger;
3962
+ const bad = optimum === MeterOptimum.Low ? MeterLevel.Danger : MeterLevel.Success;
3963
+ if (ratio >= thresholds.high) return bad;
3964
+ return ratio >= thresholds.low ? MeterLevel.Warning : good;
3965
+ }
3966
+ function meterModel({ value = 0, min = 0, max = 100, valueText, thresholds, optimum = MeterOptimum.Low, parts }) {
3967
+ const span = max - min || 1;
3968
+ const ratio = clamp01((value - min) / span);
3969
+ const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: clamp01(p.value / span), words: p.valueText ?? percent(p.value / span) }));
3970
+ let start = 0;
3971
+ const arcs = parts ? shares.map((p) => {
3972
+ const arc = { part: p.i, len: p.ratio, start };
3973
+ start += p.ratio;
3974
+ return arc;
3975
+ }) : [{ part: void 0, len: ratio, start: 0 }];
3976
+ return { ratio, level: levelOf(ratio, thresholds, optimum), words: valueText ?? percent(ratio), shares, arcs };
3977
+ }
3978
+
3979
+ // src/native/meter.tsx
3953
3980
  import { jsx as jsx41, jsxs as jsxs37 } from "react/jsx-runtime";
3954
3981
  var THICK = { sm: 6, md: 8, lg: 12 };
3955
3982
  var RING = { sm: 56, md: 96, lg: 144 };
3956
3983
  var partColor = (t, i) => [t.accent, t.warning, t.success, t.textSecondary, t.danger][i % 5];
3984
+ var levelColor = (t, level) => ({ ok: t.accent, success: t.success, warning: t.warning, danger: t.danger })[level];
3957
3985
  function Meter({ label, value = 0, min = 0, max = 100, valueText, thresholds, optimum = "low", parts, variant = "bar", size = "md", showLabel, children, style }) {
3958
3986
  const { theme } = useTheme();
3959
- const span = max - min || 1;
3960
- const ratio = Math.min(1, Math.max(0, (value - min) / span));
3961
- const level = !thresholds ? "ok" : ratio >= thresholds.high ? optimum === "low" ? "danger" : "success" : ratio >= thresholds.low ? "warning" : optimum === "low" ? "success" : "danger";
3962
- const tone3 = { ok: theme.accent, success: theme.success, warning: theme.warning, danger: theme.danger }[level];
3963
- const words2 = valueText ?? `${Math.round(ratio * 100)} %`;
3964
- const shares = (parts ?? []).map((p, i) => ({ ...p, color: partColor(theme, i), ratio: Math.min(1, Math.max(0, p.value / span)), words: p.valueText ?? `${Math.round(p.value / span * 100)} %` }));
3987
+ const { ratio, level, words: words2, shares: modelShares, arcs } = meterModel({ value, min, max, valueText, thresholds, optimum, parts });
3988
+ const tone3 = levelColor(theme, level);
3989
+ const shares = modelShares.map((p) => ({ ...p, color: partColor(theme, p.i) }));
3965
3990
  const a11y = parts ? { role: "group", "aria-label": `${label}: ${shares.map((p) => `${p.label} ${p.words}`).join(", ")}` } : { role: "meter", "aria-label": label, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Math.min(max, Math.max(min, value)), "aria-valuetext": words2 };
3966
3991
  const legend = parts ? /* @__PURE__ */ jsx41(View36, { style: styles29.legend, "aria-hidden": true, children: shares.map((p, i) => /* @__PURE__ */ jsxs37(View36, { style: styles29.legendItem, children: [
3967
3992
  /* @__PURE__ */ jsx41(View36, { style: [styles29.swatch, { backgroundColor: p.color }] }),
@@ -3972,17 +3997,11 @@ function Meter({ label, value = 0, min = 0, max = 100, valueText, thresholds, op
3972
3997
  const side = RING[size];
3973
3998
  const r = 44;
3974
3999
  const c = 2 * Math.PI * r;
3975
- let offset = 0;
3976
- const arcs = parts ? shares.map((p) => {
3977
- const arc = { color: p.color, len: p.ratio, start: offset };
3978
- offset += p.ratio;
3979
- return arc;
3980
- }) : [{ color: tone3, len: ratio, start: 0 }];
3981
4000
  return /* @__PURE__ */ jsxs37(View36, { accessible: true, ...a11y, style: [styles29.ringRoot, style], children: [
3982
4001
  /* @__PURE__ */ jsxs37(View36, { style: { width: side, height: side }, children: [
3983
4002
  /* @__PURE__ */ jsxs37(Svg19, { width: side, height: side, viewBox: "0 0 100 100", style: { transform: [{ rotate: "-90deg" }] }, children: [
3984
4003
  /* @__PURE__ */ jsx41(Circle12, { cx: 50, cy: 50, r, fill: "none", stroke: theme.borderDefault, strokeWidth: 9 }),
3985
- arcs.map((a, i) => /* @__PURE__ */ jsx41(Circle12, { cx: 50, cy: 50, r, fill: "none", stroke: a.color, strokeWidth: 9, strokeDasharray: `${Math.max(0, a.len * c - (parts ? 2 : 0))} ${c}`, strokeDashoffset: -a.start * c, strokeLinecap: parts ? "butt" : "round" }, i))
4004
+ arcs.map((arc, i) => /* @__PURE__ */ jsx41(Circle12, { cx: 50, cy: 50, r, fill: "none", stroke: arc.part === void 0 ? tone3 : partColor(theme, arc.part), strokeWidth: 9, strokeDasharray: `${Math.max(0, arc.len * c - (parts ? 2 : 0))} ${c}`, strokeDashoffset: -arc.start * c, strokeLinecap: parts ? "butt" : "round" }, i))
3986
4005
  ] }),
3987
4006
  children !== void 0 ? /* @__PURE__ */ jsx41(View36, { style: [StyleSheet29.absoluteFill, styles29.center], children }) : null
3988
4007
  ] }),
@@ -4496,7 +4515,7 @@ function Progress({ variant = "bar", size = "md", value, max = 100, label, value
4496
4515
  const id = useSvgId("kz-progress-");
4497
4516
  const known = typeof value === "number" && Number.isFinite(value);
4498
4517
  const ratio = known ? Math.min(1, Math.max(0, value / (max || 1))) : 0;
4499
- const percent = Math.round(ratio * 100);
4518
+ const percent2 = Math.round(ratio * 100);
4500
4519
  const [progress] = React32.useState(() => new Animated18.Value(ratio));
4501
4520
  const [spin] = React32.useState(() => new Animated18.Value(0));
4502
4521
  const [track, setTrack] = React32.useState(0);
@@ -4525,7 +4544,7 @@ function Progress({ variant = "bar", size = "md", value, max = 100, label, value
4525
4544
  "aria-valuemin": known ? 0 : void 0,
4526
4545
  "aria-valuemax": known ? max : void 0,
4527
4546
  "aria-valuenow": known ? Math.min(max, Math.max(0, value)) : void 0,
4528
- "aria-valuetext": known ? valueText ?? `${percent}%` : valueText
4547
+ "aria-valuetext": known ? valueText ?? `${percent2}%` : valueText
4529
4548
  };
4530
4549
  const gradient = /* @__PURE__ */ jsx50(Defs13, { children: /* @__PURE__ */ jsxs45(LinearGradient10, { id, x1: "0", y1: "0", x2: "1", y2: variant === "ring" ? "1" : "0", children: [
4531
4550
  /* @__PURE__ */ jsx50(Stop13, { offset: "0", stopColor: theme.accent }),