Keep Block.V3 and QuestionBox.V2 around for a while longer

This commit is contained in:
Tessa Kelly 2023-02-01 10:45:47 -07:00
parent f455f980b7
commit 573b8ecb4e
5 changed files with 957 additions and 0 deletions

View File

@ -1 +1,3 @@
Nri.Ui.Block.V3,upgrade to V4
Nri.Ui.QuestionBox.V2,upgrade to V3
Nri.Ui.Tabs.V6,upgrade to V7

1 Nri.Ui.Tabs.V6 Nri.Ui.Block.V3 upgrade to V7 upgrade to V4
1 Nri.Ui.Block.V3 upgrade to V4
2 Nri.Ui.QuestionBox.V2 upgrade to V3
3 Nri.Ui.Tabs.V6 Nri.Ui.Tabs.V6 upgrade to V7 upgrade to V7

View File

@ -11,6 +11,7 @@
"Nri.Ui.AnimatedIcon.V1",
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Balloon.V2",
"Nri.Ui.Block.V3",
"Nri.Ui.Block.V4",
"Nri.Ui.BreadCrumbs.V2",
"Nri.Ui.Button.V10",
@ -54,6 +55,7 @@
"Nri.Ui.Panel.V1",
"Nri.Ui.Pennant.V2",
"Nri.Ui.PremiumCheckbox.V8",
"Nri.Ui.QuestionBox.V2",
"Nri.Ui.QuestionBox.V3",
"Nri.Ui.RadioButton.V4",
"Nri.Ui.RingGauge.V1",

615
src/Nri/Ui/Block/V3.elm Normal file
View File

@ -0,0 +1,615 @@
module Nri.Ui.Block.V3 exposing
( view, Attribute
, plaintext
, Content, content
, phrase, wordWithQuestionBox
, space
, blank, blankWithQuestionBox
, emphasize
, label
, labelId, labelContentId
, LabelPosition, getLabelPositions, labelPosition
, yellow, cyan, magenta, green, blue, purple, brown
, withQuestionBox
)
{-|
@docs view, Attribute
## Content
@docs plaintext
@docs Content, content
@docs phrase, wordWithQuestionBox
@docs space
@docs blank, blankWithQuestionBox
## Content customization
@docs emphasize
## Labels & positioning
@docs label
You will need these helpers if you want to prevent label overlaps. (Which is to say -- anytime you have labels!)
@docs labelId, labelContentId
@docs LabelPosition, getLabelPositions, labelPosition
### Visual customization
@docs yellow, cyan, magenta, green, blue, purple, brown
### Add a question box
@docs withQuestionBox
-}
import Accessibility.Styled exposing (..)
import Browser.Dom as Dom
import Css exposing (Color)
import Dict exposing (Dict)
import Html.Styled.Attributes exposing (css)
import List.Extra
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 exposing (nriDescription)
import Nri.Ui.Mark.V2 as Mark exposing (Mark)
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.QuestionBox.V2 as QuestionBox
import Position exposing (xOffsetPx)
{-|
Block.view [ Block.plaintext "Hello, world!" ]
-}
view : List (Attribute msg) -> Html msg
view attributes =
attributes
|> List.foldl (\(Attribute attribute) b -> attribute b) defaultConfig
|> render
-- Attributes
{-| Provide the main content of the block as a plain-text string. You can also use `content` for more complex cases, including a blank appearing within an emphasis.
-}
plaintext : String -> Attribute msg
plaintext content_ =
Attribute <| \config -> { config | content = parseString content_ }
{-| Use `content` for more complex block views, for instance when a blank appears within an emphasis block. Prefer to use `plaintext` when possible for better readability.
Block.view
[ Block.emphasize
, Block.content (Block.phrase "Hello, " ++ Block.blank :: Block.phrase "!" ) ]
]
-}
content : List (Content msg) -> Attribute msg
content content_ =
Attribute <| \config -> { config | content = content_ }
{-| Mark content as emphasized.
-}
emphasize : Attribute msg
emphasize =
Attribute <| \config -> { config | emphasize = True }
{-| -}
label : String -> Attribute msg
label label_ =
Attribute <| \config -> { config | label = Just label_, emphasize = True }
{-| Use `getLabelPositions` to construct this value.
-}
type alias LabelPosition =
{ totalHeight : Float
, arrowHeight : Float
, zIndex : Int
, xOffset : Float
}
{-| Use `getLabelPositions` to calculate what these values should be.
-}
labelPosition : Maybe LabelPosition -> Attribute msg
labelPosition offset =
Attribute <| \config -> { config | labelPosition = offset }
{-| -}
labelContentId : String -> String
labelContentId labelId_ =
labelId_ ++ "-label-content"
{-| Determine where to position labels in order to dynamically avoid overlapping content.
- First, we add ids to block with labels with `Block.labelId`.
- Say we added `Block.labelId "example-id"`, then we will use `Browser.Dom.getElement "example-id"` and `Browser.Dom.getElement (Block.labelContentId "example-id")` to construct a record in the shape { label : Dom.Element, labelContent : Dom.Element }. We store this record in a dictionary keyed by ids (e.g., "example-id") with measurements for all other labels.
`getLabelPositions` will return a dictionary of values (keyed by label ids) whose values can be passed directly to `labelPosition` for positioning.
-}
getLabelPositions :
Dict String { label : Dom.Element, labelContent : Dom.Element }
-> Dict String LabelPosition
getLabelPositions labelMeasurementsById =
let
startingArrowHeight =
8
in
Dict.toList labelMeasurementsById
|> splitByOverlaps
|> List.concatMap
(\row ->
let
maxRowIndex =
List.length row - 1
in
row
-- Put the widest elements higher visually to avoid overlaps
|> List.sortBy (Tuple.second >> .labelContent >> .element >> .width)
|> List.foldl
(\( idString, e ) ( index, height, acc ) ->
( index + 1
, height + e.labelContent.element.height + 4
, ( idString
, { totalHeight = height + e.labelContent.element.height
, arrowHeight = height
, zIndex = maxRowIndex - index
, xOffset = xOffsetPx e.label
}
)
:: acc
)
)
( 0, startingArrowHeight, [] )
|> (\( _, _, v ) -> v)
)
|> Dict.fromList
{-| Group the elements whose bottom edges are at the same height. This ensures that we only offset labels against other labels in the same visual line of content.
Then, for elements in the same row, group elements with horizontal overlaps .
-}
splitByOverlaps : List ( id, { a | label : Dom.Element } ) -> List (List ( id, { a | label : Dom.Element } ))
splitByOverlaps =
-- consider the elements from top to bottom
groupWithSort (\( _, a ) -> a.label.element.y + a.label.element.height)
(\( _, a ) ( _, b ) ->
(a.label.element.y + a.label.element.height) == (b.label.element.y + b.label.element.height)
)
>> List.concatMap
-- consider the elements from left to right
(groupWithSort (\( _, a ) -> a.label.element.x)
(\( _, a ) ( _, b ) ->
(a.label.element.x + xOffsetPx a.label + a.label.element.width) >= (b.label.element.x + xOffsetPx b.label)
)
)
groupWithSort : (a -> comparable) -> (a -> a -> Bool) -> List a -> List (List a)
groupWithSort sortBy groupBy =
List.sortBy sortBy
>> List.Extra.groupWhile groupBy
>> List.map (\( first, rem ) -> first :: rem)
-- Content
{-| -}
type Content msg
= Word (List (QuestionBox.Attribute msg)) (Maybe Dom.Element) String
| Blank (List (QuestionBox.Attribute msg)) (Maybe Dom.Element)
| FullHeightBlank
parseString : String -> List (Content msg)
parseString =
String.split " "
>> List.intersperse " "
>> List.filter (\str -> str /= "")
>> List.map (Word [] Nothing)
renderContent :
{ config | questionBoxElement : Maybe Dom.Element }
-> Content msg
-> List Css.Style
-> Html msg
renderContent config content_ markStyles =
let
marginBottom override =
case ( config.questionBoxElement, override ) of
( Nothing, Just by ) ->
Css.important (Css.marginBottom (Css.px (by.element.height + 8)))
( Just by, _ ) ->
Css.important (Css.marginBottom (Css.px (by.element.height + 8)))
_ ->
Css.batch []
viewContainer marginOverride =
blockSegmentContainer (marginBottom marginOverride :: markStyles)
in
case content_ of
Word [] _ str ->
viewContainer Nothing [ text str ]
Word questionBoxAttributes element str ->
viewContainer element
[ text str
, QuestionBox.view
(QuestionBox.pointingTo element
:: questionBoxAttributes
)
]
Blank [] _ ->
viewContainer Nothing
[ viewBlank [ Css.lineHeight (Css.num 1) ] ]
Blank questionBoxAttributes element ->
viewContainer element
[ viewBlank [ Css.lineHeight (Css.num 1) ]
, QuestionBox.view
(QuestionBox.pointingTo element
:: questionBoxAttributes
)
]
FullHeightBlank ->
viewContainer Nothing
[ viewBlank
[ Css.paddingTop topBottomSpace
, Css.paddingBottom topBottomSpace
, Css.lineHeight Css.initial
]
]
blockSegmentContainer : List Css.Style -> List (Html msg) -> Html msg
blockSegmentContainer styles =
span
[ css
(Css.whiteSpace Css.preWrap
:: Css.display Css.inlineBlock
:: Css.position Css.relative
:: styles
)
, nriDescription "block-segment-container"
]
{-| -}
phrase : String -> List (Content msg)
phrase =
parseString
{-| -}
space : Content msg
space =
Word [] Nothing " "
{-| -}
wordWithQuestionBox : String -> List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Content msg
wordWithQuestionBox str attributes element =
Word attributes element str
{-| You will only need to use this helper if you're also using `content` to construct a more complex Block. For a less complex blank Block, don't include content or plaintext in the list of attributes.
-}
blank : Content msg
blank =
Blank [] Nothing
{-| You will only need to use this helper if you're also using `content` to construct a more complex Block. For a less complex blank Block, don't include content or plaintext in the list of attributes.
-}
blankWithQuestionBox : List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Content msg
blankWithQuestionBox attributes element =
Blank attributes element
-- Color themes
{-| -}
type Theme
= Yellow
| Cyan
| Magenta
| Green
| Blue
| Purple
| Brown
themeToPalette : Theme -> Palette
themeToPalette theme =
case theme of
Yellow ->
{ backgroundColor = Colors.highlightYellow
, borderColor = Colors.highlightYellowDark
}
Cyan ->
{ backgroundColor = Colors.highlightCyan
, borderColor = Colors.highlightCyanDark
}
Magenta ->
{ backgroundColor = Colors.highlightMagenta
, borderColor = Colors.highlightMagentaDark
}
Green ->
{ backgroundColor = Colors.highlightGreen
, borderColor = Colors.highlightGreenDark
}
Blue ->
{ backgroundColor = Colors.highlightBlue
, borderColor = Colors.highlightBlueDark
}
Purple ->
{ backgroundColor = Colors.highlightPurple
, borderColor = Colors.highlightPurpleDark
}
Brown ->
{ backgroundColor = Colors.highlightBrown
, borderColor = Colors.highlightBrownDark
}
type alias Palette =
{ backgroundColor : Color, borderColor : Color }
toMark :
{ config
| emphasize : Bool
, label : Maybe String
, content : List (Content msg)
}
-> Palette
-> Maybe Mark
toMark config { backgroundColor, borderColor } =
case ( config.label, config.content, config.emphasize ) of
( Just l, FullHeightBlank :: [], _ ) ->
Just
{ name = Just l
, startStyles = []
, styles = []
, endStyles = []
}
( Just l, _, False ) ->
Just
{ name = Just l
, startStyles = []
, styles = []
, endStyles = []
}
( _, _, True ) ->
let
borderWidth =
Css.px 1
borderStyle =
Css.dashed
in
Just
{ name = config.label
, startStyles =
[ Css.borderLeft3 borderWidth borderStyle borderColor
, Css.paddingLeft (Css.px 2)
]
, styles =
[ Css.paddingTop topBottomSpace
, Css.paddingBottom topBottomSpace
, Css.backgroundColor backgroundColor
, Css.borderTop3 borderWidth borderStyle borderColor
, Css.borderBottom3 borderWidth borderStyle borderColor
, MediaQuery.highContrastMode
[ Css.property "background-color" "Mark"
, Css.property "color" "MarkText"
, Css.property "forced-color-adjust" "none"
]
]
, endStyles =
[ Css.borderRight3 borderWidth borderStyle borderColor
, Css.paddingRight (Css.px 2)
]
}
( Nothing, _, False ) ->
Nothing
topBottomSpace : Css.Px
topBottomSpace =
Css.px 4
{-| -}
yellow : Attribute msg
yellow =
Attribute (\config -> { config | theme = Yellow })
{-| -}
cyan : Attribute msg
cyan =
Attribute (\config -> { config | theme = Cyan })
{-| -}
magenta : Attribute msg
magenta =
Attribute (\config -> { config | theme = Magenta })
{-| -}
green : Attribute msg
green =
Attribute (\config -> { config | theme = Green })
{-| -}
blue : Attribute msg
blue =
Attribute (\config -> { config | theme = Blue })
{-| -}
purple : Attribute msg
purple =
Attribute (\config -> { config | theme = Purple })
{-| -}
brown : Attribute msg
brown =
Attribute (\config -> { config | theme = Brown })
{-| -}
labelId : String -> Attribute msg
labelId id_ =
Attribute (\config -> { config | labelId = Just id_ })
{-| Pass in a list of question box attributes and the sizing (taken using Dom.Browser.getElement) of the question box
-}
withQuestionBox : List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Attribute msg
withQuestionBox attributes element =
Attribute (\config -> { config | questionBox = attributes, questionBoxElement = element })
-- Internals
{-| -}
type Attribute msg
= Attribute (Config msg -> Config msg)
defaultConfig : Config msg
defaultConfig =
{ content = [ FullHeightBlank ]
, label = Nothing
, labelId = Nothing
, labelPosition = Nothing
, theme = Yellow
, emphasize = False
, questionBoxElement = Nothing
, questionBox = []
}
type alias Config msg =
{ content : List (Content msg)
, label : Maybe String
, labelId : Maybe String
, labelPosition : Maybe LabelPosition
, theme : Theme
, emphasize : Bool
, questionBoxElement : Maybe Dom.Element
, questionBox : List (QuestionBox.Attribute msg)
}
render : Config msg -> Html msg
render config =
let
palette =
themeToPalette config.theme
maybeMark =
toMark config palette
in
span
[ css [ Css.position Css.relative ] ]
(Mark.viewWithBalloonTags
{ renderSegment = renderContent config
, backgroundColor = palette.backgroundColor
, maybeMarker = maybeMark
, labelPosition = config.labelPosition
, labelId = config.labelId
, labelContentId = Maybe.map labelContentId config.labelId
}
config.content
++ (case config.questionBox of
[] ->
[]
_ ->
[ QuestionBox.view
(QuestionBox.pointingTo config.questionBoxElement
:: config.questionBox
)
]
)
)
viewBlank : List Css.Style -> Html msg
viewBlank styles =
span
[ css
[ Css.border3 (Css.px 2) Css.dashed Colors.navy
, MediaQuery.highContrastMode
[ Css.property "border-color" "CanvasText"
, Css.property "background-color" "Canvas"
]
, Css.backgroundColor Colors.white
, Css.minWidth (Css.px 80)
, Css.display Css.inlineBlock
, Css.borderRadius (Css.px 4)
, Css.batch styles
]
]
[ blankString ]
blankString : Html msg
blankString =
span
[ css
[ Css.overflowX Css.hidden
, Css.width (Css.px 0)
, Css.display Css.inlineBlock
, Css.verticalAlign Css.bottom
]
]
[ text "blank" ]

View File

@ -0,0 +1,336 @@
module Nri.Ui.QuestionBox.V2 exposing
( view, Attribute
, id, markdown, actions, character
, standalone, pointingTo
, containerCss
, guidanceId
)
{-|
@docs view, Attribute
@docs id, markdown, actions, character
@docs standalone, pointingTo
@docs containerCss
@docs guidanceId
-}
import Accessibility.Styled.Key as Key
import Browser.Dom exposing (Element)
import Css
import Css.Global
import Html.Styled exposing (..)
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Balloon.V2 as Balloon
import Nri.Ui.Button.V10 as Button
import Nri.Ui.CharacterIcon.V1 as CharacterIcon
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 as AttributesExtra exposing (nriDescription)
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Position exposing (xOffsetPx)
{-| -}
type Attribute msg
= Attribute (Config msg -> Config msg)
type alias Config msg =
{ id : Maybe String
, markdown : Maybe String
, actions : List { label : String, onClick : msg }
, type_ : QuestionBoxType msg
, character : Maybe { name : String, icon : Svg }
, containerCss : List Css.Style
}
defaultConfig : Config msg
defaultConfig =
{ id = Nothing
, markdown = Nothing
, actions = []
, type_ = Standalone
, character = Just { name = "Panda", icon = CharacterIcon.panda }
, containerCss = [ Css.maxWidth (Css.px 440) ]
}
{-| -}
id : String -> Attribute msg
id id_ =
Attribute (\config -> { config | id = Just id_ })
{-| -}
markdown : String -> Attribute msg
markdown content =
Attribute (\config -> { config | markdown = Just content })
{-| -}
actions : List { label : String, onClick : msg } -> Attribute msg
actions actions_ =
Attribute (\config -> { config | actions = actions_ })
{-| -}
character : Maybe { name : String, icon : Svg } -> Attribute msg
character details =
Attribute (\config -> { config | character = details })
{-| -}
containerCss : List Css.Style -> Attribute msg
containerCss styles =
Attribute (\config -> { config | containerCss = config.containerCss ++ styles })
setType : QuestionBoxType msg -> Attribute msg
setType type_ =
Attribute (\config -> { config | type_ = type_ })
type QuestionBoxType msg
= Standalone
| PointingTo (Maybe Element)
{-| This is the default type of question box. It doesn't have a programmatic or direct visual relationship to any piece of content.
-}
standalone : Attribute msg
standalone =
setType Standalone
{-| This type of `QuestionBox` is absolutely positioned & has an arrow pointing to its relatively positioned ancestor.
Typically, you would use this type of `QuestionBox` type with a `Block` by way of `Block.withHtml`.
You will need to pass a measurement, taken using Dom.Browser, in order for the question box to be positioned correctly horizontally so that it doesn't get cut off by the viewport.
-}
pointingTo : Maybe Element -> Attribute msg
pointingTo element =
setType (PointingTo element)
{-| This helper is how we create an id for the guidance speech bubble element based on the `QuestionBox.id` value. Use this helper to manage focus.
When showing multiple questions in a sequence based on the user's answer, we want to move the user's focus to the guidance so that:
- Screenreader users are alerted to a new context -- the new question!
- Keyboard users's focus is somewhere convenient to answer the question (but not _on_ an answer, since we don't want accidental submissions or for the user to hit enter straight and miss the guidance!)
-}
guidanceId : String -> String
guidanceId id_ =
id_ ++ "__guidance-speech-bubble"
{-|
QuestionBox.view
[ QuestionBox.markdown "**WOW**, great component!"
]
-}
view : List (Attribute msg) -> Html msg
view attributes =
let
config =
List.foldl (\(Attribute f) a -> f a) defaultConfig attributes
in
case config.type_ of
Standalone ->
viewStandalone config
PointingTo element ->
viewPointingTo config element
{-| -}
viewStandalone : Config msg -> Html msg
viewStandalone config =
div
[ AttributesExtra.maybe Attributes.id config.id
, css config.containerCss
, nriDescription "standalone-balloon-container"
]
[ viewBalloon config
[ Balloon.nriDescription "standalone-balloon"
]
]
{-| -}
viewPointingTo : Config msg -> Maybe Element -> Html msg
viewPointingTo config element =
let
xOffset =
Maybe.map xOffsetPx element
|> Maybe.withDefault 0
in
viewBalloon config
[ Balloon.onBottom
, Balloon.nriDescription "pointing-to-balloon"
, case config.id of
Just id_ ->
Balloon.id id_
Nothing ->
Balloon.css []
, Balloon.containerCss
[ Css.position Css.absolute
, Css.top (Css.pct 100)
, Css.left (Css.pct 50)
, Css.transforms
[ Css.translateX (Css.pct -50)
, Css.translateY (Css.px 8)
]
, Css.minWidth (Css.px 300)
, Css.textAlign Css.center
, Css.batch config.containerCss
]
, Balloon.css <|
if xOffset /= 0 then
[ Css.property "transform" ("translateX(" ++ String.fromFloat xOffset ++ "px)")
]
else
[]
]
viewBalloon : Config msg -> List (Balloon.Attribute msg) -> Html msg
viewBalloon config attributes =
Balloon.view
([ Balloon.html
(List.filterMap identity
[ Maybe.map (viewGuidance config) config.markdown
, viewActions config.character config.actions
]
)
, Balloon.customTheme { backgroundColor = Colors.glacier, color = Colors.glacier }
, Balloon.css [ Css.padding (Css.px 0), Css.boxShadow Css.none ]
]
++ attributes
)
viewGuidance : { config | id : Maybe String, character : Maybe { name : String, icon : Svg } } -> String -> Html msg
viewGuidance config markdown_ =
case config.character of
Just character_ ->
div
[ css
[ Css.displayFlex
, Css.justifyContent Css.flexEnd
, Css.margin (Css.px 8)
, Css.marginRight (Css.px 20)
, Css.position Css.relative
]
]
[ viewCharacter character_
, viewSpeechBubble config
[ Balloon.markdown markdown_
, Balloon.onLeft
, Balloon.alignArrowEnd
, Balloon.css [ Css.minHeight (Css.px 46) ]
]
]
Nothing ->
viewSpeechBubble config
[ Balloon.markdown markdown_
, Balloon.css [ Css.margin2 (Css.px 10) (Css.px 20) ]
]
viewSpeechBubble : { config | id : Maybe String } -> List (Balloon.Attribute msg) -> Html msg
viewSpeechBubble config extraAttributes =
Balloon.view
([ Balloon.nriDescription "guidance-speech-bubble"
, Balloon.white
, Balloon.css
[ Css.borderRadius (Css.px 16)
, Css.padding (Css.px 10)
, Css.boxShadow Css.none
, Css.Global.children [ Css.Global.p [ Css.margin Css.zero ] ]
]
, Balloon.custom
[ AttributesExtra.maybe (guidanceId >> Attributes.id) config.id
, Key.tabbable False
]
]
++ extraAttributes
)
viewCharacter : { name : String, icon : Svg } -> Html msg
viewCharacter { name, icon } =
icon
|> Svg.withLabel (name ++ " says, ")
|> Svg.withWidth (Css.px 50)
|> Svg.withHeight (Css.px 70)
|> Svg.withCss
[ Css.position Css.absolute
, Css.bottom (Css.px -18)
, Css.right (Css.px -48)
]
|> Svg.toHtml
viewActions : Maybe character -> List { label : String, onClick : msg } -> Maybe (Html msg)
viewActions maybeCharacter actions_ =
let
containerStyles =
[ Css.backgroundColor Colors.frost
, Css.border3 (Css.px 1) Css.solid Colors.glacier
, Css.borderBottomRightRadius (Css.px 8)
, Css.borderBottomLeftRadius (Css.px 8)
, Css.margin Css.zero
, case maybeCharacter of
Just _ ->
Css.padding4 (Css.px 10) (Css.px 30) (Css.px 10) (Css.px 10)
Nothing ->
Css.padding2 (Css.px 10) (Css.px 20)
, Css.listStyle Css.none
, Css.displayFlex
, Css.property "gap" "10px"
, Css.flexDirection Css.column
]
in
case actions_ of
[] ->
Nothing
{ label, onClick } :: [] ->
div [ css (Css.alignItems Css.center :: containerStyles) ]
[ Button.button label
[ Button.onClick onClick
, Button.unboundedWidth
, Button.small
]
]
|> Just
_ ->
ul [ css containerStyles ]
(List.map
(\{ label, onClick } ->
li []
[ Button.button label
[ Button.onClick onClick
, Button.fillContainerWidth
, Button.small
]
]
)
actions_
)
|> Just

View File

@ -7,6 +7,7 @@
"Nri.Ui.AnimatedIcon.V1",
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Balloon.V2",
"Nri.Ui.Block.V3",
"Nri.Ui.Block.V4",
"Nri.Ui.BreadCrumbs.V2",
"Nri.Ui.Button.V10",
@ -50,6 +51,7 @@
"Nri.Ui.Panel.V1",
"Nri.Ui.Pennant.V2",
"Nri.Ui.PremiumCheckbox.V8",
"Nri.Ui.QuestionBox.V2",
"Nri.Ui.QuestionBox.V3",
"Nri.Ui.RadioButton.V4",
"Nri.Ui.RingGauge.V1",