diff --git a/Dockerfile b/Dockerfile index 39db9352c6..c147dee5c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,3 +90,7 @@ COPY --link --from=release /home/app/src/playbook-website/public /home/app/src/p COPY --link --from=release /home/app/src/node_modules/@powerhome/playbook-icons/icons /home/app/src/temp-icons RUN cp -r /home/app/src/temp-icons/* /home/app/src/playbook-website/app/javascript/images/ RUN rm -rf /home/app/src/temp-icons + +COPY --link --from=release /home/app/src/node_modules/@powerhome/playbook-icons/aliases.json /home/app/src/aliases.json +RUN cp /home/app/src/aliases.json /home/app/src/playbook-website/app/javascript/aliases.json +RUN rm /home/app/src/aliases.json diff --git a/playbook-website/Gemfile.lock b/playbook-website/Gemfile.lock index 86eabcd4bd..1c352276d5 100644 --- a/playbook-website/Gemfile.lock +++ b/playbook-website/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../playbook specs: - playbook_ui (13.32.0) + playbook_ui (13.33.1) actionpack (>= 5.2.4.5) actionview (>= 5.2.4.5) activesupport (>= 5.2.4.5) diff --git a/playbook-website/app/controllers/pages_controller.rb b/playbook-website/app/controllers/pages_controller.rb index b4bf4423b5..62ec7215a5 100755 --- a/playbook-website/app/controllers/pages_controller.rb +++ b/playbook-website/app/controllers/pages_controller.rb @@ -240,7 +240,12 @@ def set_category def kit_categories @category = params[:category] - aggregate_kits.find { |item| item["category"] == @category }["components"].map { |component| component["name"] } + components = aggregate_kits.find { |item| item["category"] == @category }["components"] + filter_kits_by_status(components, status: "beta").map { |component| component["name"] } + end + + def filter_kits_by_status(components, status: nil) + components.reject { |component| status && component["status"] == status } end def set_kit @@ -344,6 +349,7 @@ def handle_kit_collection(type) @kits_array = @kits.first.split("&") params[:name] ||= @kits_array[0] @selected_kit = params[:name] + @variants = params[:variants].present? ? params[:variants].split("&") : [] @type = type render template: "pages/kit_collection", layout: "layouts/fullscreen" diff --git a/playbook-website/app/javascript/components/VisualGuidelines/Examples/BorderRadius.tsx b/playbook-website/app/javascript/components/VisualGuidelines/Examples/BorderRadius.tsx index a769c4f8e8..25f48463e3 100644 --- a/playbook-website/app/javascript/components/VisualGuidelines/Examples/BorderRadius.tsx +++ b/playbook-website/app/javascript/components/VisualGuidelines/Examples/BorderRadius.tsx @@ -3,13 +3,16 @@ import React from 'react' import { Background, Body, + Button, Caption, Flex, + Icon, Title, } from 'playbook-ui' import Example from '../Templates/Example' +const BORDER_RADIUS_VALUES = [ "none", "xs", "sm", "md", "lg", "xl", "rounded" ] const TOKENS = { 'Rounded': '$border_radius_rounded', 'Extra Large': '$border_radius_xl', @@ -30,7 +33,7 @@ const DATASET = [ { name: 'None', class: 'border_radius_none' }, ] -const BorderRadius = ({ tokensExample }: { tokensExample: string }) => ( +const BorderRadius = ({ example, tokensExample }: { example: string, tokensExample: string }) => ( ( text='Border Radius' /> + + ; case "border_radius": - return ; + return ; case "display": return ; case "cursor": diff --git a/playbook-website/app/javascript/components/Website/src/components/CategoryTitle/index.tsx b/playbook-website/app/javascript/components/Website/src/components/CategoryTitle/index.tsx index 056757cac9..e148e7ea52 100644 --- a/playbook-website/app/javascript/components/Website/src/components/CategoryTitle/index.tsx +++ b/playbook-website/app/javascript/components/Website/src/components/CategoryTitle/index.tsx @@ -6,13 +6,14 @@ import { linkFormat } from "../../../../../utilities/website_sidebar_helper"; import "./styles.scss"; type CategoryTitleProps = { - name: string; + category: string; }; -export const CategoryTitle = ({ name }: CategoryTitleProps): React.ReactElement => { +export const CategoryTitle = ({ category }: CategoryTitleProps): React.ReactElement => { + return ( - + <Title size={{ xs: 3, sm: 2, md: 2, lg: 2, xl: 2 }} tag="h1" text={linkFormat(category)} /> <Icon className="icon mobile" icon="circle-arrow-right" size="sm" /> <Icon className="icon desktop" icon="circle-arrow-right" size="xl" /> </Flex> diff --git a/playbook-website/app/javascript/components/Website/src/hooks/loaders.ts b/playbook-website/app/javascript/components/Website/src/hooks/loaders.ts index 427e0cf1b4..0c4a25c360 100644 --- a/playbook-website/app/javascript/components/Website/src/hooks/loaders.ts +++ b/playbook-website/app/javascript/components/Website/src/hooks/loaders.ts @@ -7,13 +7,14 @@ interface ComponentTypes { } interface CategoryTypes { - name: string; + category: string; description: string; components: ComponentTypes[]; } -const sortByName = (a: ComponentTypes, b: ComponentTypes) => - a.name.localeCompare(b.name); +const sortByName = (a: ComponentTypes, b: ComponentTypes): number => { + return a.name.localeCompare(b.name); +} const sortComponentsByName = (kitCategory: CategoryTypes) => { kitCategory.components.sort(sortByName); @@ -22,8 +23,8 @@ const sortComponentsByName = (kitCategory: CategoryTypes) => { export const ComponentsLoader: () => Promise<CategoryTypes[]> = async () => { const response = await fetch("/beta/kits.json"); const data = await response.json(); - - data.kits.sort(sortByName).forEach(sortComponentsByName); + + data.kits.forEach(sortComponentsByName); return data; }; @@ -40,9 +41,9 @@ export const CategoryLoader: ( const response = await fetch("/beta/kits.json"); const { kits } = await response.json(); - const filteredData = kits.filter( - (kit: ComponentTypes) => kit.name === params.name - )[0]; + const filteredData = kits.find( + (kit: CategoryTypes) => kit.category === params.category + ); filteredData.components.sort(sortByName); diff --git a/playbook-website/app/javascript/components/Website/src/pages/CategoryShow/index.tsx b/playbook-website/app/javascript/components/Website/src/pages/CategoryShow/index.tsx index 60debf4886..b41c78fcf1 100644 --- a/playbook-website/app/javascript/components/Website/src/pages/CategoryShow/index.tsx +++ b/playbook-website/app/javascript/components/Website/src/pages/CategoryShow/index.tsx @@ -13,13 +13,13 @@ import { Kit } from "../ComponentList" import "./styles.scss" export default function CategoryShow() { - const { components, name, description } = useLoaderData() + const { components, category, description } = useLoaderData() const [kitsToShow, setKitsToShow] = useState(components) const [platform, setPlatform] = useState('react') return ( <> - <Hero description={description} title={linkFormat(name)} /> + <Hero description={description} title={linkFormat(category)} /> <Flex align="center" @@ -39,7 +39,7 @@ export default function CategoryShow() { <Body className="previous-route" color="light">Components</Body> </NavLink> <Icon className="category-breadcrumb-icon" icon="angle-right" /> - <Body text={linkFormat(name)} /> + <Body text={linkFormat(category)} /> </Flex> {!kitsToShow.length && ( @@ -54,14 +54,17 @@ export default function CategoryShow() { )} <KitGrid> - {kitsToShow.map(({ description, name }: Kit, index: number) => ( - <KitCard - description={description} - name={name} - key={`category-${name}-${index}`} - platform={platform} - /> - ))} + {kitsToShow.filter(component => component.status === "stable") + .map(({ description, name }: Kit, index: number) => { + return( + <KitCard + description={description} + name={name} + key={`category-${name}-${index}`} + platform={platform} + /> + ) + })} </KitGrid> </PageContainer> </> diff --git a/playbook-website/app/javascript/components/Website/src/pages/ComponentList.tsx b/playbook-website/app/javascript/components/Website/src/pages/ComponentList.tsx index a1e876cb9b..2e8089d830 100644 --- a/playbook-website/app/javascript/components/Website/src/pages/ComponentList.tsx +++ b/playbook-website/app/javascript/components/Website/src/pages/ComponentList.tsx @@ -10,7 +10,7 @@ import { PageContainer } from "../components/PageContainer" import { CategoryTitle } from "../components/CategoryTitle" export type Kit = { - name: string; + category: string; components: { name: string; description: string; @@ -65,14 +65,14 @@ export default function ComponentList() { /> </Flex> ) : ( - kitsToShow.map(({ name, components }: Kit, index: number) => ( + kitsToShow.map(({ category, components }: Kit, index: number) => ( <section className="category mb_xl" - key={`${name}-${index}`} - id={name} + key={`${category}-${index}`} + id={category} > - <NavLink to={`/beta/kit_category/${name}`}> - <CategoryTitle name={name} /> + <NavLink to={`/beta/kit_category/${category}`}> + <CategoryTitle category={category} /> </NavLink> <KitGrid> {components diff --git a/playbook-website/app/javascript/components/Website/src/pages/IconList/index.tsx b/playbook-website/app/javascript/components/Website/src/pages/IconList/index.tsx index 149015f4f7..179a4ee8f7 100644 --- a/playbook-website/app/javascript/components/Website/src/pages/IconList/index.tsx +++ b/playbook-website/app/javascript/components/Website/src/pages/IconList/index.tsx @@ -237,12 +237,54 @@ export default function IconList() { </Flex> + <Flex + justify='center' + marginX={{ lg: "sm", xl: "sm" }} + paddingLeft="xs" + paddingTop="sm" + > + <FlexItem alignSelf='stretch' maxWidth='xxl' flexGrow={1}> + <Title paddingBottom="sm" size={3} tag='h3' text='Other Props' /> + </FlexItem> + </Flex> + <Flex + justify='center' + marginX={{ lg: "sm", xl: "sm" }} + paddingLeft="xs" + > + <FlexItem alignSelf='stretch' maxWidth='xxl' flexGrow={1}> + <Flex> + <Card + marginRight='sm' + hover={{ shadow: "deeper" }} + cursor='pointer' + > + <Icon icon="roofing" size="2x" pull="left" /> + </Card> + <Card + marginRight='sm' + hover={{ shadow: "deeper" }} + cursor='pointer' + > + <Icon icon="roofing" size="2x" pull="right" /> + </Card> + <Card + marginRight='sm' + hover={{ shadow: "deeper" }} + cursor='pointer' + > + <Icon icon="roofing" size="2x" fixedWidth /> + </Card> + </Flex> + </FlexItem> + </Flex> <Flex justify='center' marginX={{ lg: "sm", xl: "sm" }} paddingLeft="xs" + paddingTop="sm" > <FlexItem alignSelf='stretch' maxWidth='xxl' flexGrow={1}> <Title paddingBottom="sm" size={3} tag='h3' text='Color' /> diff --git a/playbook-website/app/javascript/packs/app.js b/playbook-website/app/javascript/packs/app.js index 112cf28094..234bc0e9ff 100644 --- a/playbook-website/app/javascript/packs/app.js +++ b/playbook-website/app/javascript/packs/app.js @@ -40,7 +40,7 @@ const router = createBrowserRouter( <Route element={<CategoryShow />} loader={CategoryLoader} - path="kit_category/:name" + path="kit_category/:category" /> <Route element={<IconList />} diff --git a/playbook-website/app/views/pages/code_snippets/border_radius_jsx.txt b/playbook-website/app/views/pages/code_snippets/border_radius_jsx.txt new file mode 100644 index 0000000000..76461c5131 --- /dev/null +++ b/playbook-website/app/views/pages/code_snippets/border_radius_jsx.txt @@ -0,0 +1 @@ +<Card borderRadius="md"> Card Content </Card> \ No newline at end of file diff --git a/playbook-website/app/views/pages/kit_collection.html.erb b/playbook-website/app/views/pages/kit_collection.html.erb index 3a439da85a..f35e92e0e9 100644 --- a/playbook-website/app/views/pages/kit_collection.html.erb +++ b/playbook-website/app/views/pages/kit_collection.html.erb @@ -2,16 +2,16 @@ <%= pb_rails("nav", props: { margin_bottom: "md", orientation: "horizontal" }) do %> <%= pb_rails("flex", props: {wrap: true}) do %> <% @kits_array.each do |kit| %> - <%= pb_rails("nav/item", props: { id: "nav-item-#{kit}", text: kit.titleize, link: kit_collection_show_path(names: params[:names], name: kit, type: @type), active: kit == @selected_kit}) %> + <%= pb_rails("nav/item", props: { id: "nav-item-#{kit}", text: kit.titleize, link: kit_collection_show_path(names: params[:names], name: kit, variants: params[:variants], type: @type), active: kit == @selected_kit}) %> <% end %> <% end %> <% end %> <div class="multi-kits-container"> <% if @type == "rails" %> - <%= pb_kit(kit: @selected_kit, dark_mode: dark_mode?) %> + <%= pb_kit(kit: @selected_kit, dark_mode: dark_mode?, variants: @variants) %> <%= pb_rails("docs/kit_api", props: { kit: @selected_kit, dark: dark_mode? }) %> <% elsif @type == "react" %> - <%= pb_kit(kit: @selected_kit, type: "react", dark_mode: dark_mode?) %> + <%= pb_kit(kit: @selected_kit, type: "react", dark_mode: dark_mode?, variants: @variants) %> <% end %> </div> <% end %> \ No newline at end of file diff --git a/playbook-website/app/views/samples/icons.html.erb b/playbook-website/app/views/samples/icons.html.erb index b217d32568..1e1a191729 100644 --- a/playbook-website/app/views/samples/icons.html.erb +++ b/playbook-website/app/views/samples/icons.html.erb @@ -28,7 +28,7 @@ <% end %> <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> <%= pb_rails("card/card_body", props: { padding: "md", }) do %> - <%= pb_rails("icon", props: { icon: "nitro" }) %> + <%= pb_rails("icon", props: { icon: "nitro-n" }) %> <% end %> <% end %> <% end %> @@ -92,6 +92,39 @@ <% end %> <% end %> +<%= pb_rails("flex", props: { padding_left: "md"} ) do %> + <%= pb_rails("flex/flex_item") do %> + <%= pb_rails("title", props: {size: 3, text: "Pull/Width", padding_bottom: "sm"})%> + <% end %> +<% end %> +<%= pb_rails("flex", props: { padding_left: "md" }) do %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "powergon-p", size: "2x", pull: "right" }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "powergon-p", size: "2x", pull: "left" }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "powergon-p", size: "2x", fixed_width: true }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "powergon-p", size: "2x", fixed_width: false }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "powergon-p", size: "2x", border: true }) %> + <% end %> + <% end %> +<% end %> + <%= pb_rails("flex", props: { padding_left: "md"} ) do %> <%= pb_rails("flex/flex_item") do %> <%= pb_rails("title", props: {size: 3, text: "Color", padding_bottom: "sm"})%> @@ -161,3 +194,33 @@ <% end %> <% end %> <% end %> + + +<%= pb_rails("flex", props: { padding_left: "md"} ) do %> + <%= pb_rails("flex/flex_item") do %> + <%= pb_rails("title", props: {size: 3, text: "Aliases", padding_bottom: "sm"})%> + <% end %> +<% end %> + +<%= pb_rails("flex", props: { padding_left: "md" }) do %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "house", size: "2x", spin: true }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "home", size: "2x", pulse: true }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "gear", size: "2x", spin: true }) %> + <% end %> + <% end %> + <%= pb_rails("card", props: { padding: "none", header: true, margin_bottom: "sm"}) do %> + <%= pb_rails("card/card_body", props: { padding: "md" }) do %> + <%= pb_rails("icon", props: { icon: "cog", size: "2x", pulse: true }) %> + <% end %> + <% end %> +<% end %> diff --git a/playbook-website/config/application.rb b/playbook-website/config/application.rb index 48972cf51a..786e521774 100644 --- a/playbook-website/config/application.rb +++ b/playbook-website/config/application.rb @@ -28,6 +28,7 @@ class Application < Rails::Application config.load_defaults 7.0 config.icon_path = Rails.env.production? ? "app/javascript/images/" : "../node_modules/@powerhome/playbook-icons/icons" + config.icon_alias_path = Rails.env.production? ? "app/javascript/aliases.json" : "../node_modules/@powerhome/playbook-icons/aliases.json" # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files diff --git a/playbook-website/config/routes.rb b/playbook-website/config/routes.rb index 9245951447..96935b4a11 100644 --- a/playbook-website/config/routes.rb +++ b/playbook-website/config/routes.rb @@ -26,10 +26,10 @@ get "kit_category/:category/rails", to: "pages#kit_category_show_rails", as: "kit_category_show_rails" get "kit_category/:category/react", to: "pages#kit_category_show_react", as: "kit_category_show_reacts" get "kit_category/:category/swift", to: "pages#kit_category_show_swift", as: "kit_category_show_swift" - get "kit_collection/*names/:name/rails", to: "pages#kit_collection_show_rails", as: "kit_collection_show_rails" - get "kit_collection/*names/:name/react", to: "pages#kit_collection_show_react", as: "kit_collection_show_react" - get "kit_collection/*names/:name/swift", to: "pages#kit_collection_show_swift", as: "kit_collection_show_swift" - get "kit_collection/*names/(/:name)(/:type)", to: "pages#kit_collection_show_rails", defaults: { type: "rails" }, as: "kit_collection_show" + get "kit_collection/*names/:name(/*variants)/rails", to: "pages#kit_collection_show_rails", as: "kit_collection_show_rails" + get "kit_collection/*names/:name(/*variants)/react", to: "pages#kit_collection_show_react", as: "kit_collection_show_react" + get "kit_collection/*names/:name(/*variants)/swift", to: "pages#kit_collection_show_swift", as: "kit_collection_show_swift" + get "kit_collection/*names/(/:name)(/*variants)(/:type)", to: "pages#kit_collection_show_rails", defaults: { type: "rails" }, as: "kit_collection_show" # Experiments # diff --git a/playbook-website/lib/pb_doc_helper.rb b/playbook-website/lib/pb_doc_helper.rb index 9742cef3ed..6dbe428b24 100755 --- a/playbook-website/lib/pb_doc_helper.rb +++ b/playbook-website/lib/pb_doc_helper.rb @@ -6,8 +6,9 @@ def pb_kit_title(title) title.remove("pb_").titleize.tr("_", " ") end - def pb_kit(kit: "", type: "rails", show_code: true, limit_examples: false, dark_mode: false) + def pb_kit(kit: "", type: "rails", show_code: true, limit_examples: false, dark_mode: false, variants: []) examples = pb_doc_kit_examples(kit, type) + examples.select! { |elem| variants.any? { |variant| elem.key?(variant) } } unless variants.empty? examples = examples.first(1) if limit_examples examples.map do |example| pb_rails "docs/kit_example", props: { @@ -61,7 +62,7 @@ def render_pb_doc_kit(kit, type, limit_examples, code = true, dark_mode = false) #{pb_kit(kit: kit, type: type, show_code: code, limit_examples: limit_examples, dark_mode: dark_mode)}</div>") title + ui end - # rubocop:enable Style/OptionalBooleanParameter + # rubocop:enable Style/OptionalBooleanParameter private diff --git a/playbook-website/package.json b/playbook-website/package.json index 5df7c1f442..900bf7c368 100644 --- a/playbook-website/package.json +++ b/playbook-website/package.json @@ -10,8 +10,8 @@ "dependencies": { "@fortawesome/fontawesome-pro": "6.2.1", "@mapbox/mapbox-gl-draw": "^1.4.1", - "@powerhome/playbook-icons": "0.0.1-alpha.25", - "@powerhome/playbook-icons-react": "0.0.1-alpha.25", + "@powerhome/playbook-icons": "0.0.1-alpha.29", + "@powerhome/playbook-icons-react": "0.0.1-alpha.29", "@powerhome/power-fonts": "0.0.1-alpha.6", "@rails/webpacker": "5.4.3", "@svgr/webpack": "5.5.0", diff --git a/playbook-website/test/menu_yml_spec.rb b/playbook-website/test/menu_yml_spec.rb new file mode 100644 index 0000000000..0ee066d7e4 --- /dev/null +++ b/playbook-website/test/menu_yml_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "yaml" + +yaml_file_path = File.expand_path("../../../playbook/playbook-website/config/menu.yml", __dir__) + +RSpec.describe "Menu YAML File" do + let(:data) { YAML.safe_load(File.read(yaml_file_path), aliases: true) } + + it "should load YAML file without errors" do + expect(data).to_not be_nil + end + + it "should have categories defined" do + expect(data).to have_key("kits") + expect(data["kits"]).to be_an(Array) + expect(data["kits"]).to_not be_empty + end + + it "should have components defined for each category" do + data["kits"].each do |kit| + expect(kit).to have_key("category") + expect(kit["category"]).to be_a(String) + expect(kit).to have_key("components") + expect(kit["components"]).to be_an(Array) + expect(kit["components"]).to_not be_empty + + kit["components"].each do |component| + expect(component).to have_key("name") + expect(component["name"]).to be_a(String) + expect(component).to have_key("platforms") + expect(component["platforms"]).to be_an(Array) + expect(component["platforms"]).to_not be_empty + end + end + end +end diff --git a/playbook/CHANGELOG.md b/playbook/CHANGELOG.md index 77fae07d76..983d06b459 100644 --- a/playbook/CHANGELOG.md +++ b/playbook/CHANGELOG.md @@ -1,3 +1,65 @@ +# 🗂️ Tabbing Your Way Through Rails! 🛤️ +##### July 19, 2024 + +![13 33 0](https://github.com/user-attachments/assets/a5519122-1f6b-4a21-9016-0da53a2e39c2) + +Hey Rails devs! 🎉 Our Nav Kit just got an upgrade with a new tabbing variant! Now, you can navigate dynamic content like a pro. Dive in and let your content flow! 📚✨ Happy coding! 💻🎨 + +[13.33.0](https://github.com/powerhome/playbook/tree/13.33.0) full list of changes: + +**Kit Enhancements:** +- Adding Tabbing Prop to the Nav Kit [\#3521](https://github.com/powerhome/playbook/pull/3521) ([carloslimasd](https://github.com/carloslimasd)) +- Star Rating Kit: Interactive Form Variant (Rails) [\#3523](https://github.com/powerhome/playbook/pull/3523) ([nickamantia](https://github.com/nickamantia)) +- Form Pills: Match Handoff (Primary + Default) [#3505](https://github.com/powerhome/playbook/pull/3505) ([elisashapiro](https://github.com/elisashapiro)) +- Implementing Form Resetting and Blank Selection Prop [\#3529)](https://github.com/powerhome/playbook/pull/3529) ([carloslimasd](https://github.com/carloslimasd)) +- Implementing Default Value for Dropdown Kit [\#3510](https://github.com/powerhome/playbook/pull/3510) ([carloslimasd](https://github.com/carloslimasd)) + +**Fixed Bugs:** +- RTE Custom Extension Dropdown Button Bug Fix [#3528](https://github.com/powerhome/playbook/pull/3528) ([anthonymig88](https://github.com/anthonymig88)) +- Improve Consistency in Docs for Default Variant of Typeahead [\#3511](https://github.com/powerhome/playbook/pull/3511) ([anthonymig88](https://github.com/anthonymig88)) + +**Improvements:** +- Create a Doc for Border Radius Global Props [\#3534](https://github.com/powerhome/playbook/pull/3534) ([elisashapiro](https://github.com/elisashapiro)) +- Update Highcharts CustomOptions Formatting Note [\#3515](https://github.com/powerhome/playbook/pull/3515) ([anthonymig88](https://github.com/anthonymig88)) +- Add Alias Map Entrypoint [\#3514](https://github.com/powerhome/playbook/pull/3514) ([markdoeswork](https://github.com/markdoeswork)) +- Hide Beta Versions of Kits within Kit Categories (with menu.yml) [\#3513](https://github.com/powerhome/playbook/pull/3513) ([elisashapiro](https://github.com/elisashapiro)) +- Linter implementation: kits T - W [\#3504](https://github.com/powerhome/playbook/pull/3504) ([kangaree](https://github.com/kangaree)) +- Playbook Icons Styles [\#3491](https://github.com/powerhome/playbook/pull/3491) ([markdoeswork](https://github.com/markdoeswork)) + +[Full Changelog](https://github.com/powerhome/playbook/compare/13.32.0...13.33.0) + + +# 🐉 Effortless Drag and Drop Functionality with the NEW Draggable Kit! ⬆️⬇️ +##### July 10, 2024 + +![13-32-0](https://github.com/user-attachments/assets/c93a0344-ef0a-4fd2-be48-62fb5bf95f1e) + + +We are excited to introduce the new Draggable kit, a highly flexible solution for all your drag-and-drop needs! Use the simple subcomponent structure for greater flexibility or use the customized solutions for the List, Selectable List or Card kits for super easy implementation out of the box! + +[13.32.0](https://github.com/powerhome/playbook/tree/13.32.0) full list of changes: + +**Kit Enhancements:** +- Implementing Dropdown Form Validation [\#3507](https://github.com/powerhome/playbook/pull/3507) ([carloslimasd](https://github.com/carloslimasd)) + +**Fixed Bugs:** +- Remove Duplicate "block" Button in RTE on Start [\#3501](https://github.com/powerhome/playbook/pull/3501) ([kangaree](https://github.com/kangaree)) +- Hide Beta Versions of Kits within Beta Index (with menu.yml) [\#3500](https://github.com/powerhome/playbook/pull/3500) ([elisashapiro](https://github.com/elisashapiro)) +- Date Year Stacked Left Align Text Color Bug [\#3508](https://github.com/powerhome/playbook/pull/3508) ([anthonymig88](https://github.com/anthonymig88)) + +**Improvements:** +- Power Centra in Playbook Part 2 (Change Variable Settings) [\#3134](https://github.com/powerhome/playbook/pull/3134) ([jasoncypret](https://github.com/jasoncypret)) +- Add Category to Playbook Kit Generator [\#3488](https://github.com/powerhome/playbook/pull/3488) ([elisashapiro](https://github.com/elisashapiro)) +- Bump ci-kubed v8.2.0 and Convert to use Remote buildkit cache mounts [\#3413](https://github.com/powerhome/playbook/pull/3413) ([TeamTeaTime](https://github.com/TeamTeaTime)) + +**New Kits:** +- Draggable Kit [\#3487](https://github.com/powerhome/playbook/pull/3487) ([nidaqg](https://github.com/nidaqg)) + + +[Full Changelog](https://github.com/powerhome/playbook/compare/13.31.0...13.32.0) + + + # 🌁 Introducing the New Overlay Kit — A Seamless Way to Implement Overlays! ‍🌁 ##### June 28, 2024 diff --git a/playbook/Gemfile.lock b/playbook/Gemfile.lock index addaa9ac21..b1714a9701 100644 --- a/playbook/Gemfile.lock +++ b/playbook/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - playbook_ui (13.32.0) + playbook_ui (13.33.1) actionpack (>= 5.2.4.5) actionview (>= 5.2.4.5) activesupport (>= 5.2.4.5) diff --git a/playbook/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom.md b/playbook/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom.md index 7e320bedc3..0ddf91dd04 100644 --- a/playbook/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom.md +++ b/playbook/app/pb_kits/playbook/pb_bar_graph/docs/_bar_graph_custom.md @@ -1,2 +1,6 @@ The `customOptions` prop provides comprehensive access to additional [Highcharts options](https://api.highcharts.com/highcharts/) that are not explicitly defined as props. It's important to note that certain options may require specific script imports to function properly. + +Note: If you are having trouble getting any Highcharts options to work, please match the formatting of our [staticOptions](https://github.com/powerhome/playbook/blob/master/playbook/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx#L85-L141). For example, `yAxis` will need to be wrapped with square brackets. + +You may also need to override any of the [defaults](https://github.com/powerhome/playbook/blob/master/playbook/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx#L45-L73) in order to get that options to work. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap b/playbook/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap index 1d9a7763c9..a18573ba67 100644 --- a/playbook/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +++ b/playbook/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap @@ -28,7 +28,7 @@ exports[`html structure is correct 1`] = ` style="vertical-align: middle; color: rgb(193, 205, 214);" > <i - class="pb_icon_kit far fa-fw fa-lg fa-chevron-down" + class="pb_icon_kit far fa-lg fa-fw fa-lg fa-chevron-down" /> <span aria-label="chevron-down icon" diff --git a/playbook/app/pb_kits/playbook/pb_contact/_contact.tsx b/playbook/app/pb_kits/playbook/pb_contact/_contact.tsx index 2380369b54..8710c2a618 100644 --- a/playbook/app/pb_kits/playbook/pb_contact/_contact.tsx +++ b/playbook/app/pb_kits/playbook/pb_contact/_contact.tsx @@ -8,23 +8,23 @@ import Body from '../pb_body/_body' import Caption from '../pb_caption/_caption' import Icon from '../pb_icon/_icon' -const contactTypeMap: { [key: string]: string; } = { +const contactTypeMap: { [key: string]: string } = { 'cell': 'mobile', 'email': 'envelope', + 'extension': 'phone-plus', 'home': 'phone', 'work': 'phone-office', 'work-cell': 'phone-laptop', 'wrong-phone': 'phone-slash', - 'extension': 'phone-plus', } const formatContact = (contactString: string, contactType: string) => { - if (contactType == 'email') return contactString + if (contactType === 'email') return contactString const cleaned = contactString.replace(/\D/g, '') const phoneNumber = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/) - if(contactType == 'extension') { + if (contactType === 'extension') { return cleaned.match(/^\d{4}$/) } @@ -40,19 +40,20 @@ const formatContact = (contactString: string, contactType: string) => { phoneNumber[4], ].join('') } - + return null } type ContactProps = { - aria?: { [key: string]: string; }, - className?: string | string[], - contactDetail?: string, - contactType?: string, - contactValue: string, - data?: {[key: string]: string}, - htmlOptions?: {[key: string]: string | number | boolean | (() => void)}, - id?: string, + aria?: { [key: string]: string } + className?: string | string[] + contactDetail?: string + contactType?: string + contactValue: string + data?: { [key: string]: string } + dark?: boolean + htmlOptions?: { [key: string]: string | number | boolean | (() => void) } + id?: string } const Contact = (props: ContactProps): React.ReactElement => { @@ -63,8 +64,10 @@ const Contact = (props: ContactProps): React.ReactElement => { contactType, contactValue, data = {}, + dark = false, htmlOptions = {}, - id } = props + id, + } = props const ariaProps = buildAriaProps(aria) const dataProps = buildDataProps(data) const htmlProps = buildHtmlProps(htmlOptions) @@ -73,6 +76,7 @@ const Contact = (props: ContactProps): React.ReactElement => { globalProps(props), className ) + return ( <div {...ariaProps} @@ -83,25 +87,27 @@ const Contact = (props: ContactProps): React.ReactElement => { > <Body className="pb_contact_kit" - color="light" + color={"light"} + dark={dark} tag="span" > <Icon + dark={dark} fixedWidth icon={contactTypeMap[contactType] || 'phone'} /> {` ${formatContact(contactValue, contactType)} `} - { - contactDetail && + {contactDetail && ( <Caption + dark={dark} size="xs" tag="span" text={contactDetail} /> - } + )} </Body> </div> ) } -export default Contact \ No newline at end of file +export default Contact diff --git a/playbook/app/pb_kits/playbook/pb_contact/contact.html.erb b/playbook/app/pb_kits/playbook/pb_contact/contact.html.erb index 77ba57668c..838b901493 100644 --- a/playbook/app/pb_kits/playbook/pb_contact/contact.html.erb +++ b/playbook/app/pb_kits/playbook/pb_contact/contact.html.erb @@ -2,18 +2,21 @@ <%= pb_rails("body", props: { tag: "span", classname: "pb_contact_kit", - color: "light" + color: "light", + dark: object.dark }) do %> <%= pb_rails("icon", props: { icon: object.contact_icon, - fixed_width: true + fixed_width: true, + dark: object.dark }) %> <%= object.formatted_contact_value if object.contact_value %> <%= pb_rails("caption", props: { text: object.contact_detail, tag: 'span', - size: 'xs' + size: 'xs', + dark: object.dark }) if object.contact_detail %> <% end %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_date_range_inline/_date_range_inline.tsx b/playbook/app/pb_kits/playbook/pb_date_range_inline/_date_range_inline.tsx index 0fe6fed837..6f610c9bfa 100644 --- a/playbook/app/pb_kits/playbook/pb_date_range_inline/_date_range_inline.tsx +++ b/playbook/app/pb_kits/playbook/pb_date_range_inline/_date_range_inline.tsx @@ -10,16 +10,16 @@ import Caption from "../pb_caption/_caption"; import Icon from "../pb_icon/_icon"; type DateRangeInlineProps = { - className?: string; - id?: string; - data?: string; align?: "left" | "center" | "vertical"; - size?: "sm" | "xs"; + className?: string; dark?: boolean; - htmlOptions?: {[key: string]: string | number | boolean | (() => any)}; + data?: string; + endDate?: Date; + htmlOptions?: { [key: string]: string | number | boolean | (() => any) }; icon?: boolean; + id?: string; + size?: "sm" | "xs"; startDate?: Date; - endDate?: Date; }; const dateTimestamp = (dateValue: Date, includeYear: boolean) => { @@ -36,59 +36,36 @@ const dateTimeIso = (dateValue: Date) => { const DateRangeInline = (props: DateRangeInlineProps): React.ReactElement => { const { - icon = false, - dark = false, - size = "sm", align = "left", + className, + dark = false, data = {}, + endDate, htmlOptions = {}, + icon = false, + size = "sm", startDate, - endDate, - className, } = props; - const iconContent = () => { - return ( - <> - {icon && ( - <> - <Body color="light" - key={Math.random()} - tag="span" - > - <Icon - className="pb_date_range_inline_icon" - dark={dark} - fixedWidth - icon="calendar-alt" - size={size} - tag="span" - /> - </Body> - </> - )} - </> - ); - }; - const dateInCurrentYear = () => { const currentDate = new Date(); return ( - startDate.getFullYear() == endDate.getFullYear() && - startDate.getFullYear() == currentDate.getFullYear() + startDate?.getFullYear() === endDate?.getFullYear() && + startDate?.getFullYear() === currentDate.getFullYear() ); }; const dateRangeClasses = buildCss("pb_date_range_inline_kit", align); - const dataProps = buildDataProps(data) - const htmlProps = buildHtmlProps(htmlOptions) + const dataProps = buildDataProps(data); + const htmlProps = buildHtmlProps(htmlOptions); + const renderTime = (date: Date) => { return ( <time dateTime={dateTimeIso(date)}> {dateInCurrentYear() ? ( - <>{` ${dateTimestamp(date, false)} `}</> + ` ${dateTimestamp(date, false)} ` ) : ( - <>{` ${dateTimestamp(date, true)} `}</> + ` ${dateTimestamp(date, true)} ` )} </time> ); @@ -101,53 +78,83 @@ const DateRangeInline = (props: DateRangeInlineProps): React.ReactElement => { className={classnames(dateRangeClasses, globalProps(props), className)} > <div className="pb_date_range_inline_wrapper"> - {size == "xs" && ( + {size === "xs" && ( <> - {iconContent()} - <Caption dark={dark} - tag="span" - > + {icon && ( + <Caption + dark={dark} + tag="span"> + <Icon + className="pb_date_range_inline_icon" + dark={dark} + fixedWidth + icon="calendar-alt" + size={size} + tag="span" + /> + </Caption> + )} + <Caption + dark={dark} + tag="span"> {renderTime(startDate)} </Caption> - <Caption dark={dark} - tag="span" - > + <Caption + dark={dark} + tag="span"> <Icon className="pb_date_range_inline_arrow" + dark={dark} fixedWidth icon="long-arrow-right" + tag="span" /> </Caption> - <Caption dark={dark} - tag="span" - > + <Caption + dark={dark} + tag="span"> {renderTime(endDate)} </Caption> </> )} - {size == "sm" && ( + {size === "sm" && ( <> - {iconContent()} - <Body dark={dark} - tag="span" - > + {icon && ( + <Body + color={"light"} + dark={dark} + tag="span"> + <Icon + className="pb_date_range_inline_icon" + dark={dark} + fixedWidth + icon="calendar-alt" + size={size} + tag="span" + /> + </Body> + )} + <Body + dark={dark} + tag="span"> {renderTime(startDate)} </Body> - <Body color="light" - dark={dark} - tag="span" - > + <Body + color={"light"} + dark={dark} + tag="span"> <Icon className="pb_date_range_inline_arrow" dark={dark} fixedWidth icon="long-arrow-right" + tag="span" /> </Body> - <Body dark={dark} - tag="span" - > + <Body + dark={dark} + tag="span"> {renderTime(endDate)} </Body> </> diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx b/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx index 99b1347bc3..5940e22e29 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +++ b/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx @@ -22,10 +22,12 @@ import { type DropdownProps = { aria?: { [key: string]: string }; autocomplete?: boolean; + blankSelection?: string; children?: React.ReactChild[] | React.ReactChild | React.ReactElement[]; className?: string; dark?: boolean; data?: { [key: string]: string }; + defaultValue?: GenericObject; error?: string; htmlOptions?: { [key: string]: string | number | boolean | (() => void) }, id?: string; @@ -40,10 +42,12 @@ const Dropdown = (props: DropdownProps) => { const { aria = {}, autocomplete = false, + blankSelection = '', children, className, dark = false, data = {}, + defaultValue = {}, error, htmlOptions = {}, id, @@ -66,7 +70,7 @@ const Dropdown = (props: DropdownProps) => { const [isDropDownClosed, setIsDropDownClosed, toggleDropdown] = useDropdown(isClosed); const [filterItem, setFilterItem] = useState(""); - const [selected, setSelected] = useState<GenericObject>({}); + const [selected, setSelected] = useState<GenericObject>(defaultValue); const [isInputFocused, setIsInputFocused] = useState(false); const [hasTriggerSubcomponent, setHasTriggerSubcomponent] = useState(true); const [hasContainerSubcomponent, setHasContainerSubcomponent] = @@ -116,11 +120,12 @@ const Dropdown = (props: DropdownProps) => { setIsDropDownClosed(isClosed) }, [isClosed]) - const filteredOptions = options?.filter((option: GenericObject) => { + const blankSelectionOption: GenericObject = blankSelection ? [{ label: blankSelection, value: "" }] : []; + const optionsWithBlankSelection = blankSelectionOption.concat(options); + const filteredOptions = optionsWithBlankSelection?.filter((option: GenericObject) => { const label = typeof option.label === 'string' ? option.label.toLowerCase() : option.label; return String(label).toLowerCase().includes(filterItem.toLowerCase()); - } - ); + }); // For keyboard accessibility: Set focus within dropdown to selected item if it exists useEffect(() => { @@ -194,7 +199,7 @@ const Dropdown = (props: DropdownProps) => { inputWrapperRef, isDropDownClosed, isInputFocused, - options, + optionsWithBlankSelection, selected, setFocusedOptionIndex, setIsDropDownClosed, @@ -233,8 +238,8 @@ const Dropdown = (props: DropdownProps) => { <> <DropdownTrigger /> <DropdownContainer> - {options && - options?.map((option: GenericObject) => ( + {optionsWithBlankSelection && + optionsWithBlankSelection?.map((option: GenericObject) => ( <Dropdown.Option key={option.id} option={option} /> diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.html.erb b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.html.erb new file mode 100644 index 0000000000..e3405df84a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.html.erb @@ -0,0 +1,10 @@ +<% + options = [ + { label: 'United States', value: 'United States', id: 'us' }, + { label: 'Canada', value: 'Canada', id: 'ca' }, + { label: 'Pakistan', value: 'Pakistan', id: 'pk' }, + ] + +%> + +<%= pb_rails("dropdown", props: { blank_selection: "Select One...", options: options }) %> diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.jsx b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.jsx new file mode 100644 index 0000000000..a73a5e8c6a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Dropdown } from '../../' + +const DropdownBlankSelection = (props) => { + const options = [ + { + label: "United States", + value: "United States", + }, + { + label: "Canada", + value: "Canada", + }, + { + label: "Pakistan", + value: "Pakistan", + } + ]; + + return ( + <> + <Dropdown + blankSelection="Select one..." + options={options} + {...props} + /> + </> + ) +} + +export default DropdownBlankSelection diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.html.erb b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.html.erb new file mode 100644 index 0000000000..766ffb8250 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.html.erb @@ -0,0 +1,10 @@ +<% + options = [ + { label: 'United States', value: 'United States', id: 'us' }, + { label: 'Canada', value: 'Canada', id: 'ca' }, + { label: 'Pakistan', value: 'Pakistan', id: 'pk' }, + ] + +%> + +<%= pb_rails("dropdown", props: {options: options, default_value: options.last}) %> diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.jsx b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.jsx new file mode 100644 index 0000000000..303635e11c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Dropdown } from '../../' + +const DropdownDefaultValue = (props) => { + const options = [ + { + label: "United States", + value: "United States", + }, + { + label: "Canada", + value: "Canada", + }, + { + label: "Pakistan", + value: "Pakistan", + } + ]; + + return ( + <> + <Dropdown + defaultValue={options[2]} + options={options} + {...props} + /> + </> + ) +} + +export default DropdownDefaultValue diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/example.yml b/playbook/app/pb_kits/playbook/pb_dropdown/docs/example.yml index b2a2557c95..5f4d445870 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/example.yml @@ -8,6 +8,8 @@ examples: - dropdown_with_custom_trigger_rails: Custom Trigger - dropdown_with_custom_padding: Custom Option Padding - dropdown_error: Dropdown with Error + - dropdown_default_value: Default Value + - dropdown_blank_selection: Blank Selection react: - dropdown_default: Default @@ -18,6 +20,8 @@ examples: - dropdown_with_custom_trigger: Custom Trigger - dropdown_with_custom_padding: Custom Option Padding - dropdown_error: Dropdown with Error + - dropdown_default_value: Default Value + - dropdown_blank_selection: Blank Selection # - dropdown_with_autocomplete: Autocomplete # - dropdown_with_autocomplete_and_custom_display: Autocomplete with Custom Display # - dropdown_with_external_control: useDropdown Hook diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/docs/index.js b/playbook/app/pb_kits/playbook/pb_dropdown/docs/index.js index 637c6f517f..92feffe44a 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_dropdown/docs/index.js @@ -9,4 +9,6 @@ export { default as DropdownWithLabel } from './_dropdown_with_label.jsx' export { default as DropdownWithExternalControl } from './_dropdown_with_external_control.jsx' export { default as DropdownWithHook } from './_dropdown_with_hook.jsx' export { default as DropdownSubcomponentStructure } from './_dropdown_subcomponent_structure.jsx' -export { default as DropdownError } from './_dropdown_error.jsx' \ No newline at end of file +export { default as DropdownError } from './_dropdown_error.jsx' +export { default as DropdownDefaultValue } from './_dropdown_default_value.jsx' +export { default as DropdownBlankSelection } from './_dropdown_blank_selection.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb index a233b70c76..5b5a8324d7 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +++ b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb @@ -8,17 +8,21 @@ <%= pb_rails("caption", props: {text: object.label, margin_bottom:"xs"}) %> <% end %> <div class="dropdown_wrapper<%= error_class %>" style="position: relative"> - <input type="hidden" name="<%= object.name %>" id="dropdown-selected-option" value="" /> - <input id="dropdown-form-validation" name="<%= object.name %>_form_validation" value="" style="display: none" <%= object.required ? "required" : ""%> /> - + <input + data-default-value="<%= input_default_value %>" + id="dropdown-selected-option" + name="<%= object.name %>" + style="display: none" + <%= object.required ? "required" : ""%> + /> <% if content.present? %> <%= content.presence %> <%= pb_rails("body", props: { status: "negative", text: object.error }) %> <% else %> <%= pb_rails("dropdown/dropdown_trigger") %> <%= pb_rails("dropdown/dropdown_container") do %> - <% if object.options.present? %> - <% object.options.each do |option| %> + <% if options_with_blank.present? %> + <% options_with_blank.each do |option| %> <%= pb_rails("dropdown/dropdown_option", props: {option: option}) %> <% end %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.rb b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.rb index 3720b1b9d5..4f839b17f0 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.rb +++ b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown.rb @@ -10,6 +10,9 @@ class Dropdown < Playbook::KitBase prop :error, type: Playbook::Props::String prop :required, type: Playbook::Props::Boolean, default: false + prop :default_value + prop :blank_selection, type: Playbook::Props::String, + default: "" def data Hash(prop(:data)).merge(pb_dropdown: true) @@ -24,6 +27,14 @@ def classname def error_class error.present? ? " error" : "" end + + def input_default_value + default_value.present? ? default_value.transform_keys(&:to_s)["id"] : "" + end + + def options_with_blank + blank_selection.present? ? [{ id: "", value: "", label: blank_selection }] + options : options + end end end end diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb index 84fb4a2c8f..e34e562dbf 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +++ b/playbook/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb @@ -11,7 +11,7 @@ class DropdownTrigger < Playbook::KitBase prop :custom_display def data - Hash(prop(:data)).merge(dropdown_trigger: true) + Hash(prop(:data)).merge(dropdown_trigger: true, dropdown_placeholder: default_display_placeholder) end def classname diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/index.js b/playbook/app/pb_kits/playbook/pb_dropdown/index.js index a1e833822f..1a639d27c4 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/index.js +++ b/playbook/app/pb_kits/playbook/pb_dropdown/index.js @@ -8,7 +8,9 @@ const DOWN_ARROW_SELECTOR = "#dropdown_open_icon"; const UP_ARROW_SELECTOR = "#dropdown_close_icon"; const OPTION_SELECTOR = "[data-dropdown-option-label]"; const CUSTOM_DISPLAY_SELECTOR = "[data-dropdown-custom-trigger]"; -const INPUT_FORM_VALIDATION = "#dropdown-form-validation"; +const DROPDOWN_TRIGGER_DISPLAY = "#dropdown_trigger_display"; +const DROPDOWN_PLACEHOLDER = "[data-dropdown-placeholder]"; +const DROPDOWN_INPUT = "#dropdown-selected-option"; export default class PbDropdown extends PbEnhancedElement { static get selector() { @@ -21,9 +23,11 @@ export default class PbDropdown extends PbEnhancedElement { connect() { this.keyboardHandler = new PbDropdownKeyboard(this); + this.setDefaultValue(); this.bindEventListeners(); this.updateArrowDisplay(false); this.handleFormValidation(); + this.handleFormReset(); } bindEventListeners() { @@ -43,14 +47,13 @@ export default class PbDropdown extends PbEnhancedElement { handleOptionClick(event) { const option = event.target.closest(OPTION_SELECTOR); - const hiddenInput = this.element.querySelector("#dropdown-selected-option"); - const inputFormValidation = this.element.querySelector(INPUT_FORM_VALIDATION); + const hiddenInput = this.element.querySelector(DROPDOWN_INPUT); if (option) { const value = option.dataset.dropdownOptionLabel; hiddenInput.value = JSON.parse(value).id; - inputFormValidation.value = JSON.parse(value).id; - this.clearFormValidation(inputFormValidation); + this.clearFormValidation(hiddenInput); + this.onOptionSelected(value, option); } } @@ -83,9 +86,7 @@ export default class PbDropdown extends PbEnhancedElement { } onOptionSelected(value, selectedOption) { - const triggerElement = this.element.querySelector( - "#dropdown_trigger_display" - ); + const triggerElement = this.element.querySelector(DROPDOWN_TRIGGER_DISPLAY); const customDisplayElement = this.element.querySelector( "#dropdown_trigger_custom_display" ); @@ -158,14 +159,18 @@ export default class PbDropdown extends PbEnhancedElement { } handleFormValidation() { - const inputFormValidation = this.element.querySelector(INPUT_FORM_VALIDATION); - - inputFormValidation.addEventListener("invalid", function (event) { - if (inputFormValidation.hasAttribute("required") && inputFormValidation.value === "") { - event.preventDefault(); - inputFormValidation.closest(".dropdown_wrapper").classList.add("error"); - } - }, true); + const hiddenInput = this.element.querySelector(DROPDOWN_INPUT); + + hiddenInput.addEventListener( + "invalid", + function (event) { + if (hiddenInput.hasAttribute("required") && hiddenInput.value === "") { + event.preventDefault(); + hiddenInput.closest(".dropdown_wrapper").classList.add("error"); + } + }, + true + ); } clearFormValidation(input) { @@ -173,10 +178,62 @@ export default class PbDropdown extends PbEnhancedElement { const dropdownWrapperElement = input.closest(".dropdown_wrapper"); dropdownWrapperElement.classList.remove("error"); - const errorLabelElement = dropdownWrapperElement.querySelector(".pb_body_kit_negative"); + const errorLabelElement = dropdownWrapperElement.querySelector( + ".pb_body_kit_negative" + ); if (errorLabelElement) { errorLabelElement.remove(); } } } + + setDefaultValue() { + const hiddenInput = this.element.querySelector(DROPDOWN_INPUT); + const options = this.element.querySelectorAll(OPTION_SELECTOR); + + const defaultValue = hiddenInput.dataset.defaultValue || ""; + hiddenInput.value = defaultValue; + + if (defaultValue) { + const selectedOption = Array.from(options).find((option) => { + return ( + JSON.parse(option.dataset.dropdownOptionLabel).id === defaultValue + ); + }); + selectedOption.classList.add("pb_dropdown_option_selected"); + this.setTriggerElementText( + JSON.parse(selectedOption.dataset.dropdownOptionLabel).label + ); + } + } + + handleFormReset() { + const form = this.element.closest("form"); + + if (form) { + form.addEventListener("reset", () => { + this.resetDropdownValue(); + }); + } + } + + resetDropdownValue() { + const hiddenInput = this.element.querySelector(DROPDOWN_INPUT); + const options = this.element.querySelectorAll(OPTION_SELECTOR); + options.forEach((option) => { + option.classList.remove("pb_dropdown_option_selected"); + }); + + hiddenInput.value = ""; + + const defaultPlaceholder = this.element.querySelector(DROPDOWN_PLACEHOLDER); + this.setTriggerElementText(defaultPlaceholder.dataset.dropdownPlaceholder); + } + + setTriggerElementText(text) { + const triggerElement = this.element.querySelector(DROPDOWN_TRIGGER_DISPLAY); + if (triggerElement) { + triggerElement.textContent = text; + } + } } diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss index 2fc706d21f..f2a179f3e9 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss +++ b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss @@ -8,6 +8,7 @@ $selector: ".pb_form_pill"; $pb_form_pill_height: 37px; $form_pill_colors: ( primary: map-get($status_color_text, "primary"), + neutral: map-get($status_color_text, "neutral"), ); @@ -23,34 +24,71 @@ $form_pill_colors: ( cursor: pointer; @each $color_name, $color_value in $form_pill_colors { &[class*=_#{$color_name}] { - background-color: rgba($color_value, $opacity-1); + background-color: mix($color_value, $card_light, 10%); + @if ($color_name == "neutral") { + background-color: $white; + border: 1px solid $border_light; + } transition: background-color 0.2s ease; box-shadow: none; @media (hover:hover) { &:hover { - background-color: rgba($color_value, $opacity-2); + background-color: mix($color_value, $card_light, 20%); + @if ($color_name == "neutral") { + background-color: mix($neutral, $card_light, 20%); + border: 1px solid $border_light; + } + } + &:active { + background-color: mix($color_value, $card_light, 30%); + @if ($color_name == "neutral") { + background-color: mix($neutral, $card_light, 30%); + } } } #{$selector}_text { color: $color_value; + @if ($color_name == "neutral") { + color: $text_lt_default; + } padding-left: $space-sm; - padding-right: $space-sm/4; + padding-right: $space-sm/2; } #{$selector}_close { color: $color_value; - padding-left: $space-sm/2; + padding-left: $space-sm/4; padding-right: $space-sm/4; display: flex; align-items: center; - height: 100%; + // I had to temporarily change height to 27px so new hover state darker background forms a circle not an oval + // before size change (ticket 2 of 4) - change back to 100% when $pb_form_pill_height changed to 27px from 37px + height: 27px; + border-radius: 70px; cursor: pointer; + &:hover { + background-color: mix($color_value, $card_light, 40%); + @if ($color_name == "neutral") { + background-color: mix($neutral, $card_light, 60%); + } + } } #{$selector}_tag { color: $color_value; padding-left: $space-sm; + @if ($color_name == "neutral") { + color: $text_lt_default; + } } } } + &:focus { + outline: $primary solid 2px; + outline-offset: -1px; + } + &:focus-visible { + outline: $primary solid 2px; + outline-offset: -1px; + } &.small { height: fit-content; height: -moz-fit-content; @@ -70,6 +108,71 @@ $form_pill_colors: ( &::before { line-height: 21px; } } } + &.dark { + @each $color_name, $color_value in $form_pill_colors { + // In dark mode, the base patterns below are needed for the next tickets for success, warning, error, info. + // Primary and Neutral are exceptions to the general rule in the handoff + &[class*=_#{$color_name}] { + // background-color: mix($color_value, $card_dark, 10%); + // .pb_form_pill_tag { + // color: $color_name; + // } + // .pb_form_pill_close { + // color: $color_name; + // &:hover { + // background-color: mix($color_value, $card_dark, 40%); + // } + // } + // &:hover { + // background-color: mix($color_value, $card_dark, 20%); + // } + // &:active { + // background-color: mix($color_value, $card_dark, 30%); + // } + @if ($color_name == "neutral") { + background-color: transparent; + border: 1px solid $border_dark; + .pb_form_pill_text, .pb_form_pill_tag { + color: $text_dk_default; + } + .pb_form_pill_close { + color: $text_dk_default; + &:hover { + background-color: mix($neutral, $card_dark, 40%); + } + } + &:hover { + background-color: mix($white, $card_dark, 10%); + } + &:active { + background-color: mix($white, $card_dark, 20%); + } + &:focus { + border: 1px solid $primary; + } + } + @if ($color_name == "primary") { + background-color: mix($active_dark, $card_dark, 10%); + .pb_form_pill_text, .pb_form_pill_tag { + color: $active_dark; + } + .pb_form_pill_close { + color: $active_dark; + &:hover { + background-color: mix($active_dark, $card_dark, 40%); + } + } + &:hover { + background-color: mix($active_dark, $card_dark, 20px); + } + &:active { + background-color: mix($active_dark, $card_dark, 30%); + } + } + } + } + } + &[class*=lowercase] { text-transform: lowercase; } diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.test.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.test.jsx new file mode 100644 index 0000000000..7c3d2e9331 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.test.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen } from '../utilities/test-utils'; +import FormPill from './_form_pill'; + +const testId = 'formpill'; + +test('should render classname', () => { + render( + <FormPill + data={{ testid: testId }} + text="test" + /> + ) + + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass('pb_form_pill_kit_primary none') +}); + +test('displays text content', () => { + render( + <FormPill + data={{ testid: testId }} + text="test" + /> + ) + + const text = screen.getByText("test") + expect(text).toBeInTheDocument() +}); + +test('displays color variant', () => { + render( + <FormPill + color={"neutral"} + data={{ testid: testId }} + text={"test"} + /> + ) + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass(`pb_form_pill_kit_neutral none`) +}); + +test('displays size variant', () => { + render( + <FormPill + data={{ testid: testId }} + size={"small"} + text={"test"} + /> + ) + const kit = screen.getByTestId('formpill') + expect(kit).toHaveClass(`pb_form_pill_kit_primary small none`) +}); \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx index 107a122abe..92f391edcf 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx @@ -5,7 +5,7 @@ import Title from '../pb_title/_title' import Icon from '../pb_icon/_icon' import Avatar from '../pb_avatar/_avatar' import { globalProps, GlobalProps } from '../utilities/globalProps' -import { buildHtmlProps } from '../utilities/props' +import { buildDataProps, buildHtmlProps } from '../utilities/props' type FormPillProps = { className?: string, @@ -18,6 +18,9 @@ type FormPillProps = { avatarUrl?: string, size?: string, textTransform?: 'none' | 'lowercase', + color?: "primary" | "neutral", + data?: {[key: string]: string}, + tabIndex?: number, closeProps?: { onClick?: React.MouseEventHandler<HTMLSpanElement>, onMouseDown?: React.MouseEventHandler<HTMLSpanElement>, @@ -36,20 +39,26 @@ const FormPill = (props: FormPillProps): React.ReactElement => { closeProps = {}, size = '', textTransform = 'none', + color = "primary", + data = {}, + tabIndex, } = props const css = classnames( - `pb_form_pill_kit_${'primary'}`, + `pb_form_pill_kit_${color}`, globalProps(props), className, size === 'small' ? 'small' : null, textTransform, ) + const dataProps = buildDataProps(data) const htmlProps = buildHtmlProps(htmlOptions) return ( <div className={css} id={id} + tabIndex={tabIndex} + {...dataProps} {...htmlProps} > {name && diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.html.erb index 9685a0e76a..ea2ee33b81 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.html.erb @@ -1 +1,5 @@ -<%= pb_rails("form_pill", props: { text_transform: "lowercase" , text: "THIS IS A TAG" }) %> \ No newline at end of file +<%= pb_rails("form_pill", props: { + text_transform: "lowercase" , + text: "THIS IS A TAG", + tabindex: 0, +}) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.jsx index 8b4e86a938..6f1f72545b 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.jsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_example.jsx @@ -6,6 +6,7 @@ const FormPillExample = (props) => { <div> <FormPill onClick={() => alert('Click!')} + tabIndex={0} text="THIS IS A TAG" textTransform="lowercase" {...props} diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.html.erb index e20377d996..5944aa0b7e 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.html.erb @@ -2,6 +2,7 @@ name: "Anna Black", avatar_url: "https://randomuser.me/api/portraits/women/44.jpg", size: "small", + tabindex: 0, }) %> <br /> @@ -10,4 +11,5 @@ <%= pb_rails("form_pill", props: { name: "Anna Black", size: "small", + tabindex: 0, }) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.jsx index 5bb3b96d18..696c8a619f 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.jsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_size.jsx @@ -9,6 +9,7 @@ const FormPillSize = (props) => { avatarUrl="https://randomuser.me/api/portraits/women/44.jpg" name="Anna Black" size="small" + tabIndex={0} {...props} /> <br /> @@ -16,6 +17,7 @@ const FormPillSize = (props) => { <FormPill name="Anna Black" size="small" + tabIndex={0} {...props} /> </div> diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.html.erb index 631c505ba9..c2cb038771 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.html.erb @@ -1 +1,4 @@ -<%= pb_rails("form_pill", props: { text: "this is a tag" }) %> \ No newline at end of file +<%= pb_rails("form_pill", props: { + text: "this is a tag", + tabindex: 0, +}) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.jsx index fca0242613..3abf85a6ac 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.jsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_tag.jsx @@ -6,8 +6,9 @@ const FormPillDefault = (props) => { <div> <FormPill onClick={() => { -alert('Click!') -}} + alert('Click!') + }} + tabIndex={0} text="this is a tag" {...props} /> diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.html.erb index b0621965a3..5473345ca0 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.html.erb @@ -1,6 +1,7 @@ <%= pb_rails("form_pill", props: { name: "Anna Black", avatar_url: "https://randomuser.me/api/portraits/women/44.jpg", + tabindex: 0, }) %> <br /> @@ -8,4 +9,5 @@ <%= pb_rails("form_pill", props: { name: "Anna Black", + tabindex: 0, }) %> diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.jsx index f72771da18..626a44eb5c 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.jsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_user.jsx @@ -9,6 +9,7 @@ const FormPillDefault = (props) => { avatarUrl="https://randomuser.me/api/portraits/women/44.jpg" name="Anna Black" onClick={() => alert('Click!')} + tabIndex={0} {...props} /> <br /> @@ -16,6 +17,7 @@ const FormPillDefault = (props) => { <FormPill name="Anna Black" onClick={() => alert('Click!')} + tabIndex={0} {...props} /> </div> diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb index 71e74b5c4f..bb160b8b14 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb @@ -1,4 +1,4 @@ -<%= content_tag(:div, id: object.id, data: object.data, class: object.classname + object.size_class, **combined_html_options) do %> +<%= content_tag(:div, id: object.id, data: object.data, class: object.classname + object.size_class, tabindex: object.tabindex, **combined_html_options) do %> <% if object.name.present? %> <%= pb_rails("avatar", props: { name: object.name, image_url: object.avatar_url, size: "xs" }) %> <%= pb_rails("title", props: { text: object.name, size: 4, classname: "pb_form_pill_text" }) %> diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.rb b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.rb index 8a098856f1..402a516750 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.rb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.rb @@ -11,9 +11,13 @@ class FormPill < Playbook::KitBase prop :text_transform, type: Playbook::Props::Enum, values: %w[none lowercase], default: "none" + prop :color, type: Playbook::Props::Enum, + values: %w[primary neutral], + default: "primary" + prop :tabindex def classname - generate_classname("pb_form_pill_kit", "primary", name, text, text_transform) + generate_classname("pb_form_pill_kit", color, name, text, text_transform) end def display_text diff --git a/playbook/app/pb_kits/playbook/pb_icon/_icon.scss b/playbook/app/pb_kits/playbook/pb_icon/_icon.scss index 605e1c3777..953e8b5ba2 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/_icon.scss +++ b/playbook/app/pb_kits/playbook/pb_icon/_icon.scss @@ -1,3 +1,19 @@ +@import "../tokens/colors"; + +// All the merges below create $icon_colors, a map of all color tokens in colors.scss +$merge_kits1: map-merge($status_colors, $category_colors); +$merge_kits2: map-merge($merge_kits1, $product_colors); +$merge_kits3: map-merge($merge_kits2, $text_colors); +$icon_colors: map-merge($merge_kits3, $data_colors); + +.pb_custom_icon, .pb_icon_kit { + @each $color_name, $color_value in $icon_colors { + &[class*="#{$color_name}"] { + color: $color_value; + } + } +} + // Rails custom icon styles svg.pb_custom_icon { width: 1em; @@ -10,3 +26,213 @@ svg.pb_custom_icon { .pb_icon_kit_emoji { font-family: monospace; } + +$rotate-list: (90, 180, 270); + +@keyframes pb_icon_spin { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +}; + +svg { + &.pb_icon_kit, + &.pb_custom_icon{ + @each $r in $rotate-list { + &.rotate_#{$r} { + transform: rotate(#{$r}deg); + } + } + &.flip_horizontal { + transform: scaleX(-1); + } + &.flip_vertical { + transform: scaleY(-1); + } + &.flip_horizontal.flip_vertical { + transform: scaleX(-1) scaleY(-1); + } + &.svg-inline--fa { + height: 1em; + overflow: visible; + vertical-align: -.125em + } + &.svg_inverse { + color: #fff; + } + &.svg_border { + border-color: #eee; + border-radius: .1em; + border-style: solid; + border-width: .08em; + padding: .2em .25em .15em; + } + &.svg_fw { + text-align: center; + width: 1.25em + } + &.svg_li { + left: calc(2em * -1); + position: absolute; + text-align: center; + width: 2em; + line-height: inherit + } + &.pull_left { + float: left; + margin-right: .3em; + } + + &.pull_right { + float: right; + margin-left: .3em; + } + &.pulse { + animation-name: pb_icon_spin; + animation-direction: normal; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: steps(8); + } + &.spin { + animation-name: pb_icon_spin; + animation-delay: 0s; + animation-direction: normal; + animation-duration: 2s; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + &.svg_xs { + font-size: 0.75em + } + + &.svg_sm { + font-size: 0.875em + } + + &.svg_lg { + font-size: 1.25em + } + + &.svg_1x { + font-size: 1em + } + + &.svg_2x { + font-size: 2em + } + + &.svg_3x { + font-size: 3em + } + + &.svg_4x { + font-size: 4em + } + + &.svg_5x { + font-size: 5em + } + + &.svg_6x { + font-size: 6em + } + + &.svg_7x { + font-size: 7em + } + + &.svg_8x { + font-size: 8em + } + + &.svg_9x { + font-size: 9em + } + + &.svg_10x { + font-size: 10em + } + &.fa-xs { + font-size: .75em; + line-height: .0833333337em; + vertical-align: .125em + } + &.fa-sm { + font-size: .875em; + line-height: .0714285718em; + vertical-align: .0535714295em + } + &.fa-lg { + font-size: 1.25em; + line-height: .05em; + vertical-align: -.075em + } + &.fa-pull-left { + float: left; + margin-right: .3em; + } + + &.fa-pull-right { + float: right; + margin-left: .3em; + } + &.fa-li { + left: calc(2em * -1); + position: absolute; + text-align: center; + width: 2em; + line-height: inherit + } + &.svg-inline--fa.fa-li { + width: 2em; + top: .25em + } + &.svg-inline--fa.fa-fw { + width: 1.25em; + } + &.fa-fw { + text-align: center; + width: 1.25em + } + &.fa-layers { + display: inline-block; + height: 1em; + position: relative; + text-align: center; + vertical-align: -.125em; + width: 1em + } + &.fa-2x { + font-size: 2em + } + &.fa-3x { + font-size: 3em + } + &.fa-flip { + animation-name: fa-flip; + animation-delay: 0s; + animation-direction: normal; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + } + &.fa-spin { + animation-name: fa-spin; + animation-delay: 0s; + animation-direction: normal; + animation-duration: 2s; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + &.fa-pulse { + animation: fa-spin 1s infinite linear; + } + } +} diff --git a/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx b/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx index 3f56d15701..42085e59d6 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx +++ b/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx @@ -3,7 +3,6 @@ import classnames from 'classnames' import { buildAriaProps, buildDataProps, buildHtmlProps } from '../utilities/props' import { GlobalProps, globalProps } from '../utilities/globalProps' import { isValidEmoji } from '../utilities/validEmojiChecker' -import aliasesJson from './icon_aliases.json' export type IconSizes = "lg" | "xs" @@ -24,6 +23,7 @@ type IconProps = { aria?: {[key: string]: string}, border?: string, className?: string, + color?: string, customIcon?: {[key: string] :SVGElement}, data?: {[key: string]: string}, fixedWidth?: boolean, @@ -41,24 +41,75 @@ type IconProps = { spin?: boolean, } & GlobalProps -type AliasType = string | string[]; - -interface Aliases { - [key: string]: AliasType; +const flipMap = { + fa: { + horizontal: 'fa-flip-horizontal', + vertical: 'fa-flip-vertical', + both: 'fa-flip-horizontal fa-flip-vertical', + none: '' + }, + svg: { + horizontal: 'flip_horizontal', + vertical: 'flip_vertical', + both: 'flip_horizontal flip_vertical', + none: '' + } } - -interface AliasesJson { - aliases: Aliases; +const pulseMap = { + fa: 'fa-pulse', + svg: 'pulse' +} +const spinMap = { + fa: 'fa-spin', + svg: 'spin' +} +const rotateMap = { + fa: { + 90: 'fa-rotate-90', + 180: 'fa-rotate-180', + 270: 'fa-rotate-270' + }, + svg: { + 90: 'rotate_90', + 180: 'rotate_180', + 270: 'rotate_270' + } } -const aliases: AliasesJson = aliasesJson; - +const sizeMap = { + fa: { + "lg": "fa-lg", + "xs": "fa-xs", + "sm": "fa-sm", + "1x": "fa-1x", + "2x": "fa-2x", + "3x": "fa-3x", + "4x": "fa-4x", + "5x": "fa-5x", + "6x": "fa-6x", + "7x": "fa-7x", + "8x": "fa-8x", + "9x": "fa-9x", + "10x": "fa-10x", + "": "" + }, + svg: { + "lg": "svg_lg", + "xs": "svg_xs", + "sm": "svg_sm", + "1x": "svg_1x", + "2x": "svg_2x", + "3x": "svg_3x", + "4x": "svg_4x", + "5x": "svg_5x", + "6x": "svg_6x", + "7x": "svg_7x", + "8x": "svg_8x", + "9x": "svg_9x", + "10x": "svg_10x", + "": "" + } -const flipMap = { - horizontal: 'fa-flip-horizontal', - vertical: 'fa-flip-vertical', - both: 'fa-flip-horizontal fa-flip-vertical', - none: "" } declare global { @@ -66,27 +117,12 @@ declare global { var PB_ICONS: {[key: string]: React.FunctionComponent<any>} } -// Resolve alias function -const resolveAlias = (icon: string): string => { - const alias = aliases.aliases[icon]; - - if (alias) { - if (Array.isArray(alias)) { - return alias[0]; - } else { - return alias; - } - } - - return icon; -}; - - const Icon = (props: IconProps) => { const { aria = {}, border = false, className, + color, customIcon, data = {}, fixedWidth = true, @@ -104,8 +140,7 @@ const Icon = (props: IconProps) => { spin = false, } = props - const resolvedIcon = resolveAlias(icon as string) - let iconElement: ReactSVGElement | null = typeof(resolvedIcon) === "object" ? resolvedIcon : null + let iconElement: ReactSVGElement | null = typeof(icon) === "object" ? icon : null const faClasses = { 'fa-border': border, @@ -121,32 +156,59 @@ const Icon = (props: IconProps) => { if (!customIcon && !iconElement) { const PowerIcon: React.FunctionComponent<any> | undefined = - window.PB_ICONS ? window.PB_ICONS[resolvedIcon as string] : null + window.PB_ICONS ? window.PB_ICONS[icon as string] : null if (PowerIcon) { iconElement = <PowerIcon /> as ReactSVGElement } else { - faClasses[`fa-${resolvedIcon}`] = resolvedIcon as string + faClasses[`fa-${icon}`] = icon as string } } - const classes = classnames( - flipMap[flip], + const isFA = !iconElement && !customIcon + + let classes = classnames( (!iconElement && !customIcon) ? 'pb_icon_kit' : '', (iconElement || customIcon) ? 'pb_custom_icon' : fontStyle, iconElement ? 'svg-inline--fa' : '', - faClasses, + color ? `color_${color}` : '', globalProps(props), className ) + const transformClasses = classnames( + flip ? flipMap[isFA ? 'fa' : 'svg'][flip] : null, + pulse ? pulseMap[isFA ? 'fa' : 'svg'] : null, + rotation ? rotateMap[isFA ? 'fa' : 'svg'][rotation] : null, + spin ? spinMap[isFA ? 'fa' : 'svg'] : null, + size ? sizeMap[isFA ? 'fa' : 'svg'][size] : null, + border ? isFA ? 'fa-border' : 'svg_border' : null, + fixedWidth ? isFA ? 'fa-fw' : 'svg_fw' : null, + inverse ? isFA ? 'fa-inverse' : 'svg_inverse' : null, + listItem ? isFA ? 'fa-li' : 'svg_li' : null, + pull ? isFA ? `fa-pull-${pull}` : `pull_${pull}` : null, + ) + classes += ` ${transformClasses}` + + if (isFA) { + const faClassList = { + 'fa-border': border, + 'fa-inverse': inverse, + 'fa-li': listItem, + [`fa-${size}`]: size, + [`fa-pull-${pull}`]: pull, + } + faClassList[`fa-${icon}`] = icon as string + classes += ` ${classnames(faClassList)}` + } + const classesEmoji = classnames( 'pb_icon_kit_emoji', globalProps(props), className ) - aria.label ? null : aria.label = `${resolvedIcon} icon` + aria.label ? null : aria.label = `${icon} icon` const ariaProps: {[key: string]: any} = buildAriaProps(aria) const dataProps: {[key: string]: any} = buildDataProps(data) const htmlProps = buildHtmlProps(htmlOptions) @@ -168,7 +230,7 @@ const Icon = (props: IconProps) => { } </> ) - else if (isValidEmoji(resolvedIcon as string)) + else if (isValidEmoji(icon as string)) return ( <> <span @@ -177,7 +239,7 @@ const Icon = (props: IconProps) => { className={classesEmoji} id={id} > - {resolvedIcon} + {icon} </span> </> ) diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb new file mode 100644 index 0000000000..3e3b2e5520 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb @@ -0,0 +1,5 @@ +<%= pb_rails("flex", props: {orientation: "column"}) do %> + <%= pb_rails("icon", props: { icon: "user", fixed_width: true, color: "primary", padding_bottom: "sm", size: "2x" }) %> + <%= pb_rails("icon", props: { icon: "recycle", fixed_width: true, color: "data_4", padding_bottom: "sm", size: "2x" }) %> + <%= pb_rails("icon", props: { icon: "roofing", fixed_width: true, color: "product_5_background", size: "2x" }) %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx new file mode 100644 index 0000000000..d3662eb89c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx @@ -0,0 +1,34 @@ +import React from "react" +import Icon from "../_icon" + +const IconDefault = (props) => { + return ( + <div style={{ display: "flex", flexDirection: "column"}}> + <Icon + color="primary" + fixedWidth + icon="user" + paddingBottom="sm" + size="2x" + {...props} + /> + <Icon + color="data_4" + fixedWidth + icon="recycle" + paddingBottom="sm" + size="2x" + {...props} + /> + <Icon + color="product_5_background" + fixedWidth + icon="product-roofing" + size="2x" + {...props} + /> + </div> + ) +} + +export default IconDefault diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md new file mode 100644 index 0000000000..98aeb62f78 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md @@ -0,0 +1 @@ +Pass any text, status, data, product, or category Playbook <a href="https://playbook.powerapp.cloud/visual_guidelines/colors" target="_blank">color token</a> to the `color` prop to change any icon's color. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml b/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml index a2cef377dd..9d0f648d79 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml @@ -9,6 +9,7 @@ examples: - icon_sizes: Icon Sizes - icon_custom: Icon Custom - icon_fa_kit: Icon with FontAwesome Kit + - icon_color: Icon Color react: - icon_default: Icon Default @@ -20,6 +21,7 @@ examples: - icon_sizes: Icon Sizes - icon_custom: Icon Custom - icon_fa_kit: Icon with FontAwesome Kit + - icon_color: Icon Color swift: - icon_default_swift: Icon Default diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/index.js b/playbook/app/pb_kits/playbook/pb_icon/docs/index.js index 72808f09ac..8aa0aefb6a 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/index.js @@ -7,3 +7,4 @@ export { default as IconBorder } from './_icon_border.jsx' export { default as IconSizes } from './_icon_sizes.jsx' export { default as IconCustom } from './_icon_custom.jsx' export { default as IconFaKit} from './_icon_fa_kit.jsx' +export { default as IconColor } from './_icon_color.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_icon/icon.rb b/playbook/app/pb_kits/playbook/pb_icon/icon.rb index 4b688733ee..ebd1322e9e 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/icon.rb +++ b/playbook/app/pb_kits/playbook/pb_icon/icon.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Style/HashLikeCase - require "open-uri" require "json" @@ -38,8 +36,7 @@ class Icon < Playbook::KitBase default: "far" prop :spin, type: Playbook::Props::Boolean, default: false - - ALIASES = JSON.parse(File.read(Playbook::Engine.root.join("app/pb_kits/playbook/pb_icon/icon_aliases.json")))["aliases"].freeze + prop :color, type: Playbook::Props::String def valid_emoji? emoji_regex = /\p{Emoji}/ @@ -49,6 +46,7 @@ def valid_emoji? def classname generate_classname( "pb_icon_kit", + color_class, font_style_class, icon_class, border_class, @@ -69,6 +67,7 @@ def custom_icon_classname generate_classname( "pb_icon_kit", border_class, + color_class, fixed_width_class, flip_class, inverse_class, @@ -82,6 +81,14 @@ def custom_icon_classname ) end + def icon_alias_map + return unless Rails.application.config.respond_to?(:icon_alias_path) + + base_path = Rails.application.config.icon_alias_path + json = File.read(Rails.root.join(base_path)) + JSON.parse(json)["aliases"].freeze + end + def asset_path return unless Rails.application.config.respond_to?(:icon_path) @@ -100,7 +107,8 @@ def render_svg svg["aria"] = object.aria svg["height"] = "auto" svg["width"] = "auto" - doc.at_css("path")["fill"] = "currentColor" + fill_color = object.color || "currentColor" + doc.at_css("path")["fill"] = fill_color raw doc end @@ -111,7 +119,9 @@ def is_svg? private def resolve_alias(icon) - aliases = ALIASES[icon] + return icon unless icon_alias_map + + aliases = icon_alias_map[icon] return icon unless aliases if aliases.is_a?(Array) @@ -131,11 +141,13 @@ def svg_size end def border_class - border ? "fa-border" : nil + prefix = is_svg? ? "svg_border" : "fa-border" + border ? prefix : nil end def fixed_width_class - fixed_width ? "fa-fw" : nil + prefix = is_svg? ? "svg_fw" : "fa-fw" + fixed_width ? prefix : nil end def icon_class @@ -143,38 +155,45 @@ def icon_class end def inverse_class - inverse ? "fa-inverse" : nil + class_name = is_svg? ? "svg_inverse" : "fa-inverse" + inverse ? class_name : nil end def list_item_class - list_item ? "fa-li" : nil + class_name = is_svg? ? "svg_li" : "fa-li" + list_item ? class_name : nil end def flip_class + prefix = is_svg? ? "flip_" : "fa-flip-" case flip when "horizontal" - "fa-flip-horizontal" + "#{prefix}horizontal" when "vertical" - "fa-flip-vertical" + "#{prefix}vertical" when "both" - "fa-flip-horizontal fa-flip-vertical" + "#{prefix}horizontal #{prefix}vertical" end end def pull_class - pull ? "fa-pull-#{pull}" : nil + class_name = is_svg? ? "pull_#{pull}" : "fa-pull-#{pull}" + pull ? class_name : nil end def pulse_class - pulse ? "fa-pulse" : nil + class_name = is_svg? ? "pulse" : "fa-pulse" + pulse ? class_name : nil end def rotation_class - rotation ? "fa-rotate-#{rotation}" : nil + class_name = is_svg? ? "rotate_#{rotation}" : "fa-rotate-#{rotation}" + rotation ? class_name : nil end def size_class - size ? "fa-#{size}" : nil + class_name = is_svg? ? "svg_#{size}" : "fa-#{size}" + size ? class_name : nil end def font_style_class @@ -182,10 +201,13 @@ def font_style_class end def spin_class - spin ? "fa-spin" : nil + class_name = is_svg? ? "spin" : "fa-spin" + spin ? class_name : nil + end + + def color_class + color ? "color_#{color}" : nil end end end end - -# rubocop:enable Style/HashLikeCase diff --git a/playbook/app/pb_kits/playbook/pb_icon/icon.test.js b/playbook/app/pb_kits/playbook/pb_icon/icon.test.js index 2e61187286..9f872e74a8 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/icon.test.js +++ b/playbook/app/pb_kits/playbook/pb_icon/icon.test.js @@ -12,7 +12,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} fixedWidth icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -27,7 +27,7 @@ describe("Icon Kit", () => { fixedWidth icon="user" rotation={rotateProp} - /> + /> ) const kit = screen.getByTestId(testId) @@ -44,7 +44,7 @@ describe("Icon Kit", () => { fixedWidth flip="horizontal" icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -59,7 +59,7 @@ describe("Icon Kit", () => { fixedWidth icon="spinner" spin - /> + /> ) const kit = screen.getByTestId(testId) @@ -73,7 +73,7 @@ describe("Icon Kit", () => { fixedWidth icon="arrow-left" pull="left" - /> + /> ) const kit = screen.getByTestId(testId) @@ -87,7 +87,7 @@ describe("Icon Kit", () => { fixedWidth icon="arrow-left" pull="left" - /> + /> ) const kit = screen.getByTestId(testId) @@ -101,7 +101,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} fixedWidth icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -128,7 +128,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} icon="user" size={sizeProp} - /> + /> ) const kit = screen.getByTestId(testId) @@ -145,11 +145,24 @@ describe("Icon Kit", () => { fixedWidth fontStyle="fas" icon="user" - /> + /> ) const kit = screen.getByTestId(testId) expect(kit).toHaveClass("fa-user pb_icon_kit fa-fw fas") }) + test("renders with color prop", () => { + render( + <Icon + color="primary" + data={{ testid: testId }} + icon="user" + /> + ) + + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass("color_primary") + }) + }) \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_icon/icon_aliases.json b/playbook/app/pb_kits/playbook/pb_icon/icon_aliases.json deleted file mode 100644 index 411c10f0d7..0000000000 --- a/playbook/app/pb_kits/playbook/pb_icon/icon_aliases.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "aliases": { - "arrow-alt-circle-right": "circle-right", - "angles-down": "angle-double-down", - "arrow-alt-down": "down", - "arrow-alt-up": "up", - "arrow-right-long": "long-arrow-right", - "arrow-to-bottom": "arrow-down-to-line", - "arrows-h": "arrows-left-right", - "calendar-days": "calendar-alt", - "circle-arrow-right": "arrow-circle-right", - "clock-rotate-left": "history", - "close": [ - "times", - "xmark" - ], - "ellipsis-h": "ellipsis", - "exclamation-circle": "circle-exclamation", - "external-link": "arrow-up-right-from-square", - "file-lines": "file-alt", - "gear": "cog", - "home": "house", - "info-circle": "circle-info", - "map-o": "map", - "message": "comment-alt", - "minus-circle": "circle-minus", - "money": "money-bill", - "mouse-pointer": "arrow-pointer", - "nitro": "nitro-n", - "play-circle": "circle-play", - "plus-circle": "circle-plus", - "plus-square": "square-plus", - "powergon": "powergon-p", - "question-circle": "circle-question", - "roofing": "product-roofing", - "shelves": "inventory", - "th-list": "table-list" - } - } \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_nav/_nav_item.test.js b/playbook/app/pb_kits/playbook/pb_nav/_nav_item.test.js index d9e8939748..25256a6e0e 100644 --- a/playbook/app/pb_kits/playbook/pb_nav/_nav_item.test.js +++ b/playbook/app/pb_kits/playbook/pb_nav/_nav_item.test.js @@ -95,11 +95,11 @@ test('should not have a left border', () => { test('should have a right icon', () => { render(<NavDefault iconRight="angle-down" />) const kit = screen.getByTestId(itemTestId) - expect(kit).toContainHTML('<i class="pb_icon_kit far fa-fw fa-angle-down pb_nav_list_item_icon_right" />') + expect(kit).toContainHTML('<i class="pb_icon_kit far pb_nav_list_item_icon_right fa-fw fa-angle-down" />') }) test('should have a left icon', () => { render(<NavDefault iconLeft="users-class" />) const kit = screen.getByTestId(itemTestId) - expect(kit).toContainHTML('<i class="pb_icon_kit far fa-fw fa-users-class pb_nav_list_item_icon_left" />') + expect(kit).toContainHTML('<i class="pb_icon_kit far pb_nav_list_item_icon_left fa-fw fa-users-class" />') }) diff --git a/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.html.erb b/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.html.erb new file mode 100755 index 0000000000..5f2381e7ca --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.html.erb @@ -0,0 +1,48 @@ +<%= pb_rails("nav", props: { orientation: "horizontal", tabbing: true, padding_bottom: "sm" }) do %> + <%= pb_rails("nav/item", props: { text: "About", active: true, data: { pb_tab_target: "about" }, cursor: "pointer" }) %> + <%= pb_rails("nav/item", props: { text: "Case Studies", data: { pb_tab_target: "case_studies" }, cursor: "pointer" }) %> + <%= pb_rails("nav/item", props: { text: "Service", data: { pb_tab_target: "service" }, cursor: "pointer" }) %> + <%= pb_rails("nav/item", props: { text: "Contacts", data: { pb_tab_target: "contacts" }, cursor: "pointer" }) %> +<% end %> + +<div id="about"> + <%= pb_rails("body", props: { text: "This is about!" }) %> +</div> + +<div id="case_studies"> + <%= pb_rails("body", props: { text: "This is case studies!" }) %> +</div> + +<div id="service"> + <%= pb_rails("body", props: { text: "This is service!" }) %> +</div> + +<div id="contacts"> + <%= pb_rails("body", props: { text: "This is contacts!" }) %> +</div> + +<script> + // The script in the code snippet below is for demonstrating the active state and NOT needed for the kit to function. + // The active prop can be used to highlight this active state. + const navItemClass = "pb_nav_list_kit_item" + const navItemActiveClass = "pb_nav_list_kit_item_active" + const dataNavItems = "[data-pb-tab-target]" + + const navItemTabs = document.querySelectorAll(dataNavItems) + navItemTabs.forEach(navItemTab => { + navItemTab.addEventListener("click", event => { + const navItemTabs = document.querySelectorAll(dataNavItems) + navItemTabs.forEach(navItemTab => { + if (navItemTab === event.target.closest(dataNavItems)) { + navItemTab.classList.add(navItemActiveClass) + navItemTab.classList.remove(navItemClass) + } else { + if (navItemTab.classList.contains(navItemActiveClass)) { + navItemTab.classList.remove(navItemActiveClass) + navItemTab.classList.add(navItemClass) + } + } + }) + }) + }) +</script> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.md b/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.md new file mode 100755 index 0000000000..4ec62fc885 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_nav/docs/_tab_nav.md @@ -0,0 +1,3 @@ +The Nav kit can also be used to create dynamic tabbing. To do so, use the boolean `tabbing` prop as shown here. + +All divs you want to use as tabs MUST have an id attached to them. To link the tab to the nav, use the required data attribute `pb_tab_target` on each nav/item and pass it the id of the tab you want linked to that specific nav/item. See code example below to see this in action. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_nav/docs/example.yml b/playbook/app/pb_kits/playbook/pb_nav/docs/example.yml index 2fd41bb1db..a51b4ce698 100755 --- a/playbook/app/pb_kits/playbook/pb_nav/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_nav/docs/example.yml @@ -19,6 +19,7 @@ examples: - block_nav: Block - block_no_title_nav: Without Title - new_tab: Open in a New Tab + - tab_nav: Tab Nav react: - default_nav: Default diff --git a/playbook/app/pb_kits/playbook/pb_nav/index.js b/playbook/app/pb_kits/playbook/pb_nav/index.js new file mode 100644 index 0000000000..a648f205bf --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_nav/index.js @@ -0,0 +1,43 @@ +import PbEnhancedElement from '../pb_enhanced_element' + +const NAV_SELECTOR = '[data-pb-nav-tab]' +const NAV_ITEM_SELECTOR = '[data-pb-tab-target]' + +export default class PbNav extends PbEnhancedElement { + static get selector() { + return NAV_SELECTOR + } + + connect() { + this.hideAndAddEventListeners() + } + + hideAndAddEventListeners() { + const navItems = this.element.querySelectorAll(NAV_ITEM_SELECTOR) + navItems.forEach((navItem) => { + if (!navItem.className.includes('active')) { + this.changeContentDisplay(navItem.dataset.pbTabTarget, 'none') + } + + navItem.addEventListener('click', event => this.handleNavItemClick(event)) + }) + } + + handleNavItemClick(event) { + event.preventDefault() + const navItem = event.target.closest(NAV_ITEM_SELECTOR) + this.changeContentDisplay(navItem.dataset.pbTabTarget, 'block') + + const navItems = this.element.querySelectorAll(NAV_ITEM_SELECTOR) + navItems.forEach((navItemSelected) => { + if (navItem !== navItemSelected) { + this.changeContentDisplay(navItemSelected.dataset.pbTabTarget, 'none') + } + }) + } + + changeContentDisplay(contentId, display) { + const content = document.getElementById(contentId) + content.style.display = display + } +} diff --git a/playbook/app/pb_kits/playbook/pb_nav/nav.rb b/playbook/app/pb_kits/playbook/pb_nav/nav.rb index e065bbcd08..dcc9d23574 100755 --- a/playbook/app/pb_kits/playbook/pb_nav/nav.rb +++ b/playbook/app/pb_kits/playbook/pb_nav/nav.rb @@ -13,11 +13,20 @@ class Nav < Playbook::KitBase default: "normal" prop :highlight, type: Playbook::Props::Boolean, default: true prop :borderless, type: Playbook::Props::Boolean, default: false + prop :tabbing, type: Playbook::Props::Boolean, default: false def classname generate_classname("pb_nav_list", variant, orientation, highlight_class, borderless_class) end + def data + if tabbing + Hash(prop(:data)).merge(pb_nav_tab: true) + else + prop(:data) + end + end + def highlight_class highlight ? "highlight" : nil end diff --git a/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx b/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx index 07af2c4d0c..26e5b68d30 100644 --- a/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx +++ b/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx @@ -10,7 +10,7 @@ const MoreExtensionsDropdown = ({extensions}: any) => { const [showPopover, setShowPopover] = useState(false) const handleTogglePopover = () => { - setShowPopover(true) + setShowPopover(!showPopover) } const handlePopoverClose = (shouldClosePopover: boolean) => { diff --git a/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx b/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx index a355a98ffa..562c1c04e9 100644 --- a/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx +++ b/playbook/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx @@ -67,7 +67,7 @@ const toolbarDropdownItems = [ const handleTogglePopover = () => { - setShowPopover(true) + setShowPopover(!showPopover) } const handlePopoverClose = (shouldClosePopover: boolean) => { diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/_star_rating.scss b/playbook/app/pb_kits/playbook/pb_star_rating/_star_rating.scss index f40cd68086..0965960c72 100644 --- a/playbook/app/pb_kits/playbook/pb_star_rating/_star_rating.scss +++ b/playbook/app/pb_kits/playbook/pb_star_rating/_star_rating.scss @@ -48,8 +48,8 @@ $star-styles: ( - yellow_star: (color: #F9BB00), - primary_star: (color: #0056CF), + yellow_star: (color: $yellow), + primary_star: (color: $royal), suble_star_light: (color: $text_lt_default), suble_star_dark: (color: $text_dk_default), empty_star_dark: (color: $border_dark), @@ -111,4 +111,13 @@ } } } + .yellow-star-selected { + color: $yellow; + } + .primary-star-selected { + color: $royal + } + .suble-star-selected { + color: $text_lt_default; + } } diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/docs/_star_rating_interactive.html.erb b/playbook/app/pb_kits/playbook/pb_star_rating/docs/_star_rating_interactive.html.erb new file mode 100644 index 0000000000..41b1f249b1 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_star_rating/docs/_star_rating_interactive.html.erb @@ -0,0 +1 @@ +<%= pb_rails("star_rating", props: { padding_bottom: "xs", variant: "interactive" }) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/docs/example.yml b/playbook/app/pb_kits/playbook/pb_star_rating/docs/example.yml index d367d24b39..9253d7b9c7 100644 --- a/playbook/app/pb_kits/playbook/pb_star_rating/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_star_rating/docs/example.yml @@ -13,4 +13,4 @@ examples: - star_rating_background_options: Background Options - star_rating_hide: Layout Options - star_rating_number_config: Number Config - - star_rating_size_options: Size Options + - star_rating_size_options: Size Options \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/index.js b/playbook/app/pb_kits/playbook/pb_star_rating/index.js new file mode 100644 index 0000000000..34b8eb3859 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_star_rating/index.js @@ -0,0 +1,50 @@ +import PbEnhancedElement from "../pb_enhanced_element"; + +const STAR_RATING_SELECTOR = "[data-pb-star-rating]"; +const STAR_RATING_INPUT_ID = "star-rating-input"; + +export default class PbStarRating extends PbEnhancedElement { + static get selector() { + return STAR_RATING_SELECTOR; + } + + connect() { + this.element.addEventListener("click", (event) => { + const clickedStarId = event.currentTarget.id; + this.updateStarColors(clickedStarId); + this.updateHiddenInputValue(clickedStarId); + }); + } + + updateStarColors(clickedStarId) { + const allStars = document.querySelectorAll(STAR_RATING_SELECTOR); + + allStars.forEach(star => { + const starId = star.id; + const icon = star.querySelector(".interactive-star-icon"); + + if (icon) { + if (starId <= clickedStarId) { + if (star.classList.contains("yellow_star")) { + icon.classList.add("yellow-star-selected"); + } else if (star.classList.contains("primary_star")) { + icon.classList.add("primary-star-selected"); + } else if (star.classList.contains("suble_star_light")) { + icon.classList.add("suble-star-selected"); + } else { + icon.classList.add("yellow-star-selected"); + } + } else { + icon.classList.remove("yellow-star-selected", "primary-star-selected", "suble-star-selected"); + } + } + }); + } + + updateHiddenInputValue(value) { + const hiddenInput = document.getElementById(STAR_RATING_INPUT_ID); + if (hiddenInput) { + hiddenInput.value = value; + } + } +} diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.html.erb b/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.html.erb index 21703f56ba..25814366d3 100644 --- a/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.html.erb +++ b/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.html.erb @@ -28,13 +28,33 @@ <% end %> <% end %> <%= pb_rails("flex", props: { }) do %> - <% object.star_count.times do %> - <%= pb_rails("icon", props: { classname: "#{star_color} pb_star_#{size}" , custom_icon: Playbook::Engine.root.join(star_svg_path) } ) %> - <% end %> - <% object.empty_stars.times do %> - <%= pb_rails("icon", props: { classname: "#{background_star_color} pb_star_#{size}", custom_icon: Playbook::Engine.root.join(background_star_path) } ) %> + + <% if object.variant == "display" %> + + <% object.star_count.times do %> + <%= pb_rails("icon", props: { classname: "#{star_color} pb_star_#{size}" , custom_icon: Playbook::Engine.root.join(star_svg_path) } ) %> + <% end %> + <% object.empty_stars.times do %> + <%= pb_rails("icon", props: { classname: "#{background_star_color} pb_star_#{size}", custom_icon: Playbook::Engine.root.join(background_star_path) } ) %> + <% end %> + + <% else %> + <%= pb_rails("flex", props: { orientation: "column" }) do %> + <% if object.label.present? %> + <%= pb_rails("caption", props: {text: object.label, margin_bottom:"xs"}) %> + <% end %> + <input type="hidden" id="star-rating-input" value="" name="<%= object.name %>"/> + <%= pb_rails("flex", props: { orientation: "row" }) do %> + <% object.denominator.times do |index| %> + <div data-pb-star-rating id="<%= index + 1 %>" class="<%= star_color %>"> + <%= pb_rails("icon", props: { classname: "#{background_star_color} pb_star_#{size} interactive-star-icon", custom_icon: Playbook::Engine.root.join(background_star_path)} ) %> + </div> + <% end %> + <% end %> + <% end %> <% end %> <% end %> + <% if layout_option == "onestar" %> <%= content_tag(:div, class: "pb_star_rating_number_#{size}") do %> <% case object.size %> diff --git a/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.rb b/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.rb index a6320b5400..d1c8536a5f 100644 --- a/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.rb +++ b/playbook/app/pb_kits/playbook/pb_star_rating/star_rating.rb @@ -25,6 +25,12 @@ class StarRating < Playbook::KitBase values: %w[fill outline], default: "fill" + prop :variant, type: Playbook::Props::Enum, + values: %w[display interactive], + default: "display" + prop :label, type: Playbook::Props::String + prop :name, type: Playbook::Props::String + def one_decimal_rating rating.to_f.round(1) end diff --git a/playbook/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_default.html.erb b/playbook/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_default.html.erb index 8c7b894a95..563e70a4ff 100644 --- a/playbook/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_default.html.erb +++ b/playbook/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_default.html.erb @@ -1,63 +1,28 @@ -<%= pb_rails("typeahead", props: { - label: "user", - name: :foo, - data: { typeahead_example: true }, - input_options: { - classname: "my-typeahead-class", - data: { - typeahead_testing: "data field test" - }, - id: "typeahead-input-id-test", - }, -})%> - -<br><br><br> - -<%= pb_rails("card", props: { padding: "xl", data: { typeahead_example_selected_option: true } }) do %> - <%= pb_rails("body") do %> - Use the above input to search for users on Github, and see the results as you type. - <% end %> - - <%= pb_rails("body") do %> - When you make a selection, you will see it appear in the list below - <% end %> +<% + options = [ + { label: 'Orange', value: '#FFA500' }, + { label: 'Red', value: '#FF0000' }, + { label: 'Green', value: '#00FF00' }, + { label: 'Blue', value: '#0000FF' }, + ] +%> - <div data-selected-option></div> -<% end %> - -<template data-typeahead-example-result-option> - <%= pb_rails("user", props: { - name: tag(:slot, name: "name"), - orientation: "horizontal", - align: "left", - avatar_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApocMXEAAAAASUVORK5CYII=", - avatar: true - }) %> -</template> +<%= pb_rails("typeahead", props: { + id: "typeahead-default", + placeholder: "All Colors", + options: options, + label: "Colors", + name: :foo, + is_multi: false + }) +%> <%= javascript_tag defer: "defer" do %> - document.addEventListener("pb-typeahead-kit-search", function(event) { - if (!event.target.dataset || !event.target.dataset.typeaheadExample) return; - - fetch(`https://api.github.com/search/users?q=${encodeURIComponent(event.detail.searchingFor)}`) - .then(response => response.json()) - .then((result) => { - const resultOptionTemplate = document.querySelector("[data-typeahead-example-result-option]") - - event.detail.setResults((result.items || []).map((user) => { - const wrapper = resultOptionTemplate.content.cloneNode(true) - wrapper.querySelector('slot[name="name"]').replaceWith(user.login) - wrapper.querySelector('img').dataset.src = user.avatar_url - return wrapper - })) - }) + document.addEventListener("pb-typeahead-kit-typeahead-default-result-option-select", function(event) { + console.log('Single Option selected') + console.dir(event.detail) }) - - - document.addEventListener("pb-typeahead-kit-result-option-selected", function(event) { - if (!event.target.dataset.typeaheadExample) return; - - document.querySelector("[data-typeahead-example-selected-option] [data-selected-option]").innerHTML = "" - document.querySelector("[data-typeahead-example-selected-option] [data-selected-option]").innerHTML = event.detail.selected.innerHTML + document.addEventListener("pb-typeahead-kit-typeahead-default-result-clear", function() { + console.log('All options cleared') }) <% end %> diff --git a/playbook/app/pb_kits/playbook/playbook-rails.js b/playbook/app/pb_kits/playbook/playbook-rails.js index a8edc7b2b6..426ba44274 100644 --- a/playbook/app/pb_kits/playbook/playbook-rails.js +++ b/playbook/app/pb_kits/playbook/playbook-rails.js @@ -39,6 +39,12 @@ PbDropdown.start() import PbAdvancedTable from './pb_advanced_table' PbAdvancedTable.start() +import PbNav from './pb_nav' +PbNav.start() + +import PbStarRating from './pb_star_rating' +PbStarRating.start() + import 'flatpickr' // React-Rendered Rails Kits ===== diff --git a/playbook/lib/playbook/forms/builder.rb b/playbook/lib/playbook/forms/builder.rb index 0fe0f9b3df..171d4ca34c 100644 --- a/playbook/lib/playbook/forms/builder.rb +++ b/playbook/lib/playbook/forms/builder.rb @@ -14,6 +14,7 @@ class Builder < ::ActionView::Helpers::FormBuilder require_relative "builder/multi_level_select_field" require_relative "builder/phone_number_field" require_relative "builder/dropdown_field" + require_relative "builder/star_rating_field" prepend(FormFieldBuilder.new(:email_field, kit_name: "text_input")) prepend(FormFieldBuilder.new(:number_field, kit_name: "text_input")) diff --git a/playbook/lib/playbook/forms/builder/star_rating_field.rb b/playbook/lib/playbook/forms/builder/star_rating_field.rb new file mode 100644 index 0000000000..3c3c266cbe --- /dev/null +++ b/playbook/lib/playbook/forms/builder/star_rating_field.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Playbook + module Forms + class Builder + def star_rating_field(name, props: {}) + props[:name] = name + props[:margin_bottom] = "sm" + props[:label] = @template.label(@object_name, name) if props[:label] == true + @template.pb_rails("star_rating", props: props) + end + end + end +end diff --git a/playbook/lib/playbook/version.rb b/playbook/lib/playbook/version.rb index 9fb09c0e2b..4b8749fc0b 100644 --- a/playbook/lib/playbook/version.rb +++ b/playbook/lib/playbook/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Playbook - PREVIOUS_VERSION = "13.31.0" - VERSION = "13.32.0" + PREVIOUS_VERSION = "13.33.0" + VERSION = "13.33.1" end diff --git a/playbook/package.json b/playbook/package.json index 003f7c9b5e..24fa5afc57 100644 --- a/playbook/package.json +++ b/playbook/package.json @@ -1,6 +1,6 @@ { "name": "playbook-ui", - "version": "13.32.0", + "version": "13.33.1", "description": "Nitro's Design System", "main": "./dist/playbook.js", "types": "./dist/types/index.d.ts", diff --git a/playbook/spec/pb_kits/playbook/kits/form_pill_spec.rb b/playbook/spec/pb_kits/playbook/kits/form_pill_spec.rb index 51b13b8dc4..93a14afc41 100644 --- a/playbook/spec/pb_kits/playbook/kits/form_pill_spec.rb +++ b/playbook/spec/pb_kits/playbook/kits/form_pill_spec.rb @@ -9,11 +9,13 @@ it { is_expected.to define_prop(:name) } it { is_expected.to define_prop(:avatar_url) } it { is_expected.to define_prop(:size) } + it { is_expected.to define_prop(:color) } describe "#classname" do it "returns namespaced class name", :aggregate_failures do expect(subject.new({}).classname).to eq "pb_form_pill_kit_primary_none" expect(subject.new(classname: "additional_class").classname).to eq "pb_form_pill_kit_primary_none additional_class" + expect(subject.new(color: "neutral").classname).to eq "pb_form_pill_kit_neutral_none" end end end diff --git a/playbook/spec/pb_kits/playbook/kits/icon_spec.rb b/playbook/spec/pb_kits/playbook/kits/icon_spec.rb index ff9a2a1df8..a8ca6e5785 100644 --- a/playbook/spec/pb_kits/playbook/kits/icon_spec.rb +++ b/playbook/spec/pb_kits/playbook/kits/icon_spec.rb @@ -51,6 +51,11 @@ is_expected.to define_prop(:spin) .of_type(Playbook::Props::Boolean) } + it { + is_expected.to define_prop(:color) + .of_type(Playbook::Props::String) + .with_default(nil) + } describe "#custom_icon" do it "returns an icon with custom data-collapsible-main attribute", :aggregate_failures do @@ -83,5 +88,16 @@ expect(subject.new(icon: icon, spin: true).classname).to eq "pb_icon_kit far fa-user fa-spin" expect(subject.new(icon: icon, classname: "additional_class").classname).to eq "pb_icon_kit far fa-user additional_class" end + it "includes color class when color prop is provided", :aggregate_failures do + icon = "user" + color = "primary" + + expect(subject.new(icon: icon, color: color).classname).to include "color_primary" + end + it "does not include color class when color prop is not provided", :aggregate_failures do + icon = "user" + + expect(subject.new(icon: icon).classname).not_to include "color_" + end end end diff --git a/playbook/spec/pb_kits/playbook/kits/star_rating_spec.rb b/playbook/spec/pb_kits/playbook/kits/star_rating_spec.rb index e8f292c1ab..8cc8d43625 100644 --- a/playbook/spec/pb_kits/playbook/kits/star_rating_spec.rb +++ b/playbook/spec/pb_kits/playbook/kits/star_rating_spec.rb @@ -39,6 +39,12 @@ .with_values("xs", "sm", "md", "lg") } + it { + is_expected.to define_enum_prop(:variant) + .with_default("display") + .with_values("display", "interactive") + } + describe "#classname" do it "returns namespaced class name", :aggregate_failures do expect(subject.new({}).classname).to eq "pb_star_rating_kit" diff --git a/yarn.lock b/yarn.lock index d3fbb0fd5a..d01a7638a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2832,15 +2832,15 @@ resolved "https://npm.powerapp.cloud/@powerhome/eslint-config/-/eslint-config-0.1.0.tgz#f1e1151481f51421186ba8caad99fadb24b1fe51" integrity sha512-8e5DRgnKPbJ+ofAtqjUPZTxcgHg/TVf52WeNqAGBUXSS4QWHemL6fj1n/YHfAIvx3aMUhFwrw2lUNofUocgK3A== -"@powerhome/playbook-icons-react@0.0.1-alpha.25": - version "0.0.1-alpha.25" - resolved "https://npm.powerapp.cloud/@powerhome/playbook-icons-react/-/3a700f9b6d6427aefe0fb94039e292cea462fe9e#3a700f9b6d6427aefe0fb94039e292cea462fe9e" - integrity sha512-0h53TdW/PrUrwYFxonE0dIN9vupqAVLwBv70hSrRkrKb8chuLc/+kBtL15MeSJ+9cWIXF0ocFOHBlNTc25hmyw== - -"@powerhome/playbook-icons@0.0.1-alpha.25": - version "0.0.1-alpha.25" - resolved "https://npm.powerapp.cloud/@powerhome/playbook-icons/-/261d2f40561f1d4d0505e4af18892916941ed1a0#261d2f40561f1d4d0505e4af18892916941ed1a0" - integrity sha512-LtnN8E/yHaXzjQG+sZfpUbGA9Xdqpwhx8aFSPvg0lGf7phOoNFHSmjXhK0a36bZnPn0tIGBct9LhSzp6zhyVBA== +"@powerhome/playbook-icons-react@0.0.1-alpha.29": + version "0.0.1-alpha.29" + resolved "https://npm.powerapp.cloud/@powerhome/playbook-icons-react/-/ce27570207d2c1381e225ecb4266cb0c7e391e17#ce27570207d2c1381e225ecb4266cb0c7e391e17" + integrity sha512-GwsEN+rgyJejOCw1a0lwLCwXIjd20F19SiWC3YpzQSPiDYJQ8dCrZeFAt3K0ulttPIzZ0j7rCayN9A0Yy9ISiw== + +"@powerhome/playbook-icons@0.0.1-alpha.29": + version "0.0.1-alpha.29" + resolved "https://npm.powerapp.cloud/@powerhome/playbook-icons/-/a2e3f857e6c0c6a3ca601f391e66a77cc54f759d#a2e3f857e6c0c6a3ca601f391e66a77cc54f759d" + integrity sha512-IqhDXWZe2+Oeggka5thMt0scmQylFVrCDzXlymjwd6uxsjFNiejaUi8sV+rrJ73TTTdCAAOzKLj3K8F4yZcboQ== "@powerhome/power-fonts@0.0.1-alpha.6": version "0.0.1-alpha.6"