From a463189052ee0a84d8b6b6a4424fb6a2f50776c5 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sat, 10 Aug 2024 10:06:25 -0700 Subject: [PATCH] add area carpet fixture. Add carpet roll calculator test. refactor carpet roll as own component. add icons. --- __fixtures__/initialProducts.ts | 7 ++ app/(tabs)/_layout.tsx | 48 +++++--- assets/images/icons/carpet-roll-64.png | Bin 0 -> 4616 bytes .../icons/icon-carpet-roll-selected.svg | 54 +++++++++ assets/images/icons/icon-carpet-roll.svg | 54 +++++++++ components/AreaRugTag.tsx | 19 ++-- components/CarpetRollCalculator.tsx | 106 ++++++++++++++++++ components/ProductList.tsx | 14 ++- components/ProductTile.tsx | 1 + components/__tests__/AreaRugTag-test.tsx | 12 +- .../__tests__/CarpetRollCalculator-test.tsx | 44 ++++++++ .../ProductCalculatorSelector-test.tsx | 36 +++--- components/__tests__/ProductList-test.tsx | 19 +++- components/__tests__/UnitChooser-test.tsx | 2 +- features/product/productSlice.ts | 4 +- lib/product.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 59 ++++++++++ svg/icon-carpet-roll.svg | 54 +++++++++ 19 files changed, 471 insertions(+), 65 deletions(-) create mode 100644 assets/images/icons/carpet-roll-64.png create mode 100644 assets/images/icons/icon-carpet-roll-selected.svg create mode 100644 assets/images/icons/icon-carpet-roll.svg create mode 100644 components/CarpetRollCalculator.tsx create mode 100644 components/__tests__/CarpetRollCalculator-test.tsx create mode 100644 svg/icon-carpet-roll.svg diff --git a/__fixtures__/initialProducts.ts b/__fixtures__/initialProducts.ts index 947663a..9c8a639 100644 --- a/__fixtures__/initialProducts.ts +++ b/__fixtures__/initialProducts.ts @@ -103,4 +103,11 @@ export default [ type: "lumber", attributes: { name: "gutter spouts" }, }, + { + id: uuid.v4().valueOf(), + pricePerUnit: 0.75, + dimensions: { l: 1, w: 1, u: "ft" }, + type: "area_rug", + attributes: { name: "area rug" }, + }, ] as Array; \ No newline at end of file diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 8ac6a2c..cef7a24 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,11 +1,17 @@ -import { Tabs } from 'expo-router'; +import { Tabs } from "expo-router"; -import { Colors } from '@/constants/Colors'; -import { useColorScheme } from '@/hooks/useColorScheme'; -import { TabBarIcon } from '@/components/navigation/TabBarIcon'; -import { Provider } from 'react-redux'; -import fixtures from "@/__fixtures__/initialProducts" -import { setupStore } from '../store'; +import { Colors } from "@/constants/Colors"; +import { useColorScheme } from "@/hooks/useColorScheme"; +import { TabBarIcon } from "@/components/navigation/TabBarIcon"; +import { Provider } from "react-redux"; +import fixtures from "@/__fixtures__/initialProducts"; +import { setupStore } from "../store"; +const CarpetRoleSvg = require("@/assets/images/icons/icon-carpet-roll.svg"); +const CarpetRoleSelectedSvg = require("@/assets/images/icons/icon-carpet-roll-selected.svg"); + +const CarpetRollIcon = ({ selected }: { selected: boolean }) => { + return selected ? CarpetRoleSelectedSvg : CarpetRoleSvg; +}; export default function TabLayout() { const colorScheme = useColorScheme(); @@ -17,24 +23,40 @@ export default function TabLayout() { + }} + > ( - + ), }} /> ( - + + ), + }} + /> + ( + ), }} /> diff --git a/assets/images/icons/carpet-roll-64.png b/assets/images/icons/carpet-roll-64.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3c34a629a4bbb3cfb7ed7291d6f4890bb03927 GIT binary patch literal 4616 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 literal 0 HcmV?d00001 diff --git a/assets/images/icons/icon-carpet-roll-selected.svg b/assets/images/icons/icon-carpet-roll-selected.svg new file mode 100644 index 0000000..400b573 --- /dev/null +++ b/assets/images/icons/icon-carpet-roll-selected.svg @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/assets/images/icons/icon-carpet-roll.svg b/assets/images/icons/icon-carpet-roll.svg new file mode 100644 index 0000000..90d57e3 --- /dev/null +++ b/assets/images/icons/icon-carpet-roll.svg @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/components/AreaRugTag.tsx b/components/AreaRugTag.tsx index 1557750..cda750a 100644 --- a/components/AreaRugTag.tsx +++ b/components/AreaRugTag.tsx @@ -1,17 +1,12 @@ import { area_t } from "@/lib/dimensions"; +import { Product } from "@/lib/product"; import convert, { Area, Length } from "convert"; import dayjs, { Dayjs } from "dayjs"; import { StyleSheet, Text, View } from "react-native"; export type AreaRugTagProps = { dimensions: area_t, - price_per_area: { - price: number, - per: { - n: number, - u: Area, - } - }, + product: Product, date?: Dayjs currencySymbol?: string }; @@ -19,17 +14,17 @@ export type AreaRugTagProps = { export const AreaRugTag = (props: AreaRugTagProps) => { const date = props.date || dayjs(); const square = props.dimensions.l * props.dimensions.w; - const areaUnits = `square ${props.dimensions.u}`; - const square2 = convert(square, areaUnits as Area).to(props.price_per_area.per.u) - const price = (square2 / props.price_per_area.per.n) * props.price_per_area.price; + const areaUnits = `sq ${props.dimensions.u}`; + const square2 = convert(square, areaUnits as Area).to("sq " + props.product.dimensions.u as Area) + const price = (square2 / props.product.pricePerUnit) * props.product.pricePerUnit; const sPrice = price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }) const currencySymbol = props.currencySymbol || "$"; return ( - - {props.dimensions.l} x {props.dimensions.w} + + {Math.round(props.dimensions.l)} x {Math.round(props.dimensions.w)} {currencySymbol} {sPrice} {date.format("YYYY/MM/DD")} [Curent Tag Color] diff --git a/components/CarpetRollCalculator.tsx b/components/CarpetRollCalculator.tsx new file mode 100644 index 0000000..02ca7f4 --- /dev/null +++ b/components/CarpetRollCalculator.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from "react"; +import { View, Text, TextInput, Button, StyleSheet } from "react-native"; +import { + productPriceFor, + priceDisplay, + pricePerUnitDisplay, + Product, +} from "@/lib/product"; +import { selectProducts } from "@/features/product/productSlice"; +import { area_t, diameterToLength, length_t } from "@/lib/dimensions"; +import { useAppSelector } from "../app/store"; +import { AreaRugTag } from "@/components/AreaRugTag"; +import { Length } from "convert"; +import ProductList from "@/components/ProductList"; + +const DEFAULT_UNIT: Length = "ft"; + +export const CarpetRollCalculator = () => { + const products = useAppSelector(selectProducts); + + const [width, setWidth] = useState(0); + const [outerDiameter, setOuterDiameter] = useState({ + l: 0, + u: DEFAULT_UNIT, + }); + const [innerDiameter, setInnerDiameter] = useState({ + l: 0, + u: DEFAULT_UNIT, + }); + const [numRings, setNumRings] = useState(0); + const [price, setPrice] = useState(0); + const [rugDimensions, setRugDimensions] = useState({ + u: DEFAULT_UNIT, + w: 0, + l: 0, + }); + const [selectedProduct, setSelectedProduct] = useState(null); + const [units, setUnits] = useState(DEFAULT_UNIT); + + useEffect(() => { + console.log(`recalculating...`); + const dimens = { + l: diameterToLength(outerDiameter, innerDiameter, numRings).l || 0.0, + w: width || selectedProduct?.dimensions.l || 0.0, + u: units || selectedProduct?.dimensions.u || "ft", + }; + console.dir(dimens); + setRugDimensions(dimens); + }, [outerDiameter, innerDiameter, width, numRings, selectedProduct, units]); + + return ( + + {selectedProduct && ( + + )} + + Length Calculation + + Outer Diameter: + + setOuterDiameter({ l: Number(text), u: units }) + } + /> + Inner Diameter: + + setInnerDiameter({ l: Number(text), u: units }) + } + /> + Number of rings: + setNumRings(Number(text))} + /> + + + + Width: + setWidth(Number(text))} + /> + + Price: {priceDisplay(price)} + + {selectedProduct ? pricePerUnitDisplay(selectedProduct) : "0.00"} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + padding: 20, + }, +}); + +export default CarpetRollCalculator; diff --git a/components/ProductList.tsx b/components/ProductList.tsx index 7c3a7ba..b0a98d6 100644 --- a/components/ProductList.tsx +++ b/components/ProductList.tsx @@ -1,22 +1,26 @@ import { ScrollView, StyleSheet } from "react-native"; import { ProductTile } from "./ProductTile"; -import { Product } from "@/lib/product"; +import { Product, product_type_t } from "@/lib/product"; import { useState } from "react"; import { selectProducts } from "@/features/product/productSlice"; import { useAppSelector } from "@/app/store"; export type ProductSelectionProps = { onProductSelected?: (product: Product) => any; + productType?: product_type_t; }; export default function ProductList({ + productType, onProductSelected, }: ProductSelectionProps) { const [activeProduct, setActiveProduct] = useState(null as null | Product); - const products = useAppSelector(selectProducts).filter(p => !!p).filter((p) => { - console.dir(p); - return !!p.dimensions; - }); + const products = useAppSelector(selectProducts) + .filter((p) => !!p) + .filter((p: Product) => productType ? p.type === productType : true) + .filter((p) => { + return !!p.dimensions; + }); function doOnProductSelected(product: Product) { setActiveProduct(product); diff --git a/components/ProductTile.tsx b/components/ProductTile.tsx index 0c660c0..bc2df14 100644 --- a/components/ProductTile.tsx +++ b/components/ProductTile.tsx @@ -28,6 +28,7 @@ export function ProductTile ({product, onProductSelected, isActive} : ProductTil return ( onProductSelected && onProductSelected(product)}> {product.attributes?.name || `Product ${product.id}`} ({priceDisplay}) diff --git a/components/__tests__/AreaRugTag-test.tsx b/components/__tests__/AreaRugTag-test.tsx index 3a1beb5..bec3a26 100644 --- a/components/__tests__/AreaRugTag-test.tsx +++ b/components/__tests__/AreaRugTag-test.tsx @@ -1,21 +1,23 @@ -import React from 'react'; import { render, screen } from '@testing-library/react-native'; import { AreaRugTag } from '@/components/AreaRugTag'; import { area_t } from '@/lib/dimensions'; import dayjs from 'dayjs'; -import { Area } from 'convert'; + +import initialProducts from '@/__fixtures__/initialProducts'; +import { Product } from '@/lib/product'; describe('AreaRugTag', () => { it('renders correctly with dimensions, price per area, date and currency symbol', () => { - const dimensions: area_t = { l: 10, w: 20, u: 'foot' }; - const pricePerArea = { price: 100, per: { n: 1, u: 'square foot' as Area } }; + const dimensions: area_t = { l: 10, w: 20, u: 'ft' }; const date = dayjs(); const currencySymbol = '$'; + const product = initialProducts.find(p => "area_rug" === p.type) as Product; + render( diff --git a/components/__tests__/CarpetRollCalculator-test.tsx b/components/__tests__/CarpetRollCalculator-test.tsx new file mode 100644 index 0000000..6c94dea --- /dev/null +++ b/components/__tests__/CarpetRollCalculator-test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { render, fireEvent, screen, within } from "@testing-library/react-native"; +import CarpetRollCalculator from "@/components/CarpetRollCalculator"; +import { renderWithProviders } from "@/lib/rendering"; + +import allProducts from "@/__fixtures__/initialProducts"; +import { Product, pricePerUnitDisplay } from "@/lib/product"; +import initialProducts from "@/__fixtures__/initialProducts"; + +const areaRugProducts = allProducts.filter((p) => "area_rug" === p.type); + +describe("CarpetRollCalculator", () => { + it("should render correctly", () => { + renderWithProviders(, { + products: initialProducts, + }); + + const areaRug = initialProducts.find(p => p.type === 'area_rug') as Product; + const areaRugLabel = `product ${areaRug.id}`; + fireEvent.press(screen.getByLabelText(areaRugLabel)); + + // Test the interaction with the width input + const widthInput = screen.getByLabelText("width"); + fireEvent.changeText(widthInput, "10"); + + // Test the interaction with the outer diameter input + const outerDiameterInput = screen.getByLabelText("outer diameter"); + fireEvent.changeText(outerDiameterInput, "3"); + + // Test the interaction with the inner diameter input + const innerDiameterInput = screen.getByLabelText("inner diameter"); + fireEvent.changeText(innerDiameterInput, "1"); + + // Test the interaction with the number of rings input + const numRingsInput = screen.getByLabelText("number of rings"); + fireEvent.changeText(numRingsInput, "5"); + + jest.advanceTimersByTime(3000); + + // Test the interaction with the price display + const {getByText} = within(screen.getByLabelText("area rug price")); + expect(getByText(/\$.*58.*\..*19.*/)).toBeTruthy(); + }); +}); diff --git a/components/__tests__/ProductCalculatorSelector-test.tsx b/components/__tests__/ProductCalculatorSelector-test.tsx index d23050e..f8cb373 100644 --- a/components/__tests__/ProductCalculatorSelector-test.tsx +++ b/components/__tests__/ProductCalculatorSelector-test.tsx @@ -2,34 +2,27 @@ import { render, fireEvent, screen, act, within } from '@testing-library/react-n import { Provider } from 'react-redux'; import ProductCalculatorSelector from '@/components/ProductCalculatorSelector'; import { renderWithProviders } from '@/lib/rendering'; -import { Product } from '@/lib/product'; +import { Product, pricePerUnitDisplay, productPriceFor } from '@/lib/product'; + +import initialProducts from '@/__fixtures__/initialProducts'; + +const mockAreaProduct = initialProducts.find(p => 'w' in p.dimensions ) as Product +const mockLengthProduct = initialProducts.find(p => (!('w' in p.dimensions)) ) as Product describe('ProductCalculatorSelector', () => { - - const mockAreaProduct = new Product( - 100, - { l: 4, w: 8, u: "ft" }, - {"name": "area product"}, - ); - const mockLengthProduct = new Product( - 100, - { l: 4, u: "ft" }, - {"name": "length product"}, - ); - it('renders correctly', () => { renderWithProviders( (), { products: [ - mockAreaProduct.asObject, - mockLengthProduct.asObject, + mockAreaProduct, + mockLengthProduct, ], } ) expect(screen.getByText('Please select a product')).toBeTruthy(); - const label = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`; + const label = `${mockAreaProduct.attributes?.name} (${pricePerUnitDisplay(mockAreaProduct)})`; expect(screen.getByText(label)).toBeTruthy(); }); @@ -38,15 +31,14 @@ describe('ProductCalculatorSelector', () => { (), { products: [ - mockLengthProduct.asObject, - mockAreaProduct.asObject, + mockLengthProduct, + mockAreaProduct, ] } ); expect(screen.getByText('Please select a product')).toBeTruthy(); - const areaLabel = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`; - const lengthLabel = `${mockLengthProduct.attributes.name} (${mockLengthProduct.pricePerUnitDisplay})`; + const areaLabel = `${mockAreaProduct.attributes?.name} (${pricePerUnitDisplay(mockAreaProduct)})`; fireEvent.press(screen.getByText(areaLabel)); const lengthInput = screen.getByLabelText("enter length"); @@ -63,10 +55,10 @@ describe('ProductCalculatorSelector', () => { jest.advanceTimersByTime(3000); - const price = mockAreaProduct.priceFor({l: 2, w: 4, u: "ft"}); + const price = productPriceFor(mockAreaProduct, {l: 2, w: 4, u: "ft"}) const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,}); const element = screen.getByLabelText("calculated price"); const {getByText} = within(element); - expect(getByText(sPrice)).toBeTruthy(); + expect(getByText(/\$.*0.*\.10/)).toBeTruthy(); }); }); diff --git a/components/__tests__/ProductList-test.tsx b/components/__tests__/ProductList-test.tsx index 1ba179e..335ecdc 100644 --- a/components/__tests__/ProductList-test.tsx +++ b/components/__tests__/ProductList-test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { renderWithProviders } from '@/lib/rendering'; -import { Product } from '@/lib/product'; +import { Product, pricePerUnitDisplay } from '@/lib/product'; import ProductList from '@/components/ProductList'; import initialProducts from '@/__fixtures__/initialProducts'; import { screen } from '@testing-library/react-native'; @@ -15,13 +15,24 @@ describe('ProductList', () => { }); it('renders products correctly', () => { - const mockProduct = initialProducts[0]; + const mockProduct = initialProducts[0] as Product; + const label = `${mockProduct.attributes?.name} (${pricePerUnitDisplay(mockProduct)})`; const { getByText } = renderWithProviders(, { products: [mockProduct], }); + + expect(getByText(label)).toBeTruthy(); + }); - expect(getByText(mockProduct.attributes.name)).toBeTruthy(); - expect(getByText(`$${mockProduct.pricePerUnit}`)).toBeTruthy(); + it('renders only area_rug products', () => { + const areaRug = initialProducts.find(p => p.type == "area_rug") as Product; + const label = `${areaRug?.attributes?.name} (${pricePerUnitDisplay(areaRug)})`; + + renderWithProviders(, { + products: initialProducts, + }); + + expect(screen.getByText(label)).toBeTruthy(); }); }); diff --git a/components/__tests__/UnitChooser-test.tsx b/components/__tests__/UnitChooser-test.tsx index 8be883a..353ba06 100644 --- a/components/__tests__/UnitChooser-test.tsx +++ b/components/__tests__/UnitChooser-test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import UnitChooser from "../UnitChooser"; -import { Length } from 'safe-units'; +import { Length } from 'convert'; describe('UnitChooser', () => { const mockOnChoicePressed = jest.fn(); diff --git a/features/product/productSlice.ts b/features/product/productSlice.ts index 1f4b761..7d55d9f 100644 --- a/features/product/productSlice.ts +++ b/features/product/productSlice.ts @@ -1,12 +1,12 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Id, Product } from '@/lib/product'; -import { dimensions_t, ProductData } from "@/lib/dimensions_t"; +import { dimensions_t, } from "@/lib/dimensions"; import uuid from "react-native-uuid"; import { RootState } from '@/app/store'; import { Length } from 'convert'; const initialState = { - products: [] as ProductData[], + products: [] as Product[], units: "ft", }; diff --git a/lib/product.ts b/lib/product.ts index d2bab62..fa1051d 100644 --- a/lib/product.ts +++ b/lib/product.ts @@ -45,7 +45,7 @@ export type AreaRugProduct = Product & { type: "lumber" } -export function productPriceFor(product : Product, dimensions: dimensions_t, damage: number): number { +export function productPriceFor(product : Product, dimensions: dimensions_t, damage: number = 0): number { if (Number.isNaN(damage)) damage = 0; const dim = matchDimensions(dimensions, product.dimensions); return ( diff --git a/package.json b/package.json index 1b607f6..8d6105e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@babel/core": "^7.20.0", "@babel/preset-typescript": "^7.24.7", "@testing-library/react-native": "^12.5.1", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1375574..c553971 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ devDependencies: '@testing-library/react-native': specifier: ^12.5.1 version: 12.5.1(jest@29.7.0)(react-native@0.74.3)(react-test-renderer@18.2.0)(react@18.2.0) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -2977,6 +2980,20 @@ packages: dependencies: '@sinonjs/commons': 3.0.1 + /@testing-library/dom@10.4.0: + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==, tarball: https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/runtime': 7.24.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + /@testing-library/react-native@12.5.1(jest@29.7.0)(react-native@0.74.3)(react-test-renderer@18.2.0)(react@18.2.0): resolution: {integrity: sha512-PApr3f6DmSJF/EIiWYZfcBzuy6w7fK8TW4a6KfQHTeAcfZ6lADtRO7R0QM5WI+b7tJ33JvIPgzCg1MiuRz4v0g==, tarball: https://registry.npmjs.org/@testing-library/react-native/-/react-native-12.5.1.tgz} peerDependencies: @@ -2997,11 +3014,24 @@ packages: redent: 3.0.0 dev: true + /@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0): + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==, tarball: https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 10.4.0 + dev: true + /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==, tarball: https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz} engines: {node: '>= 10'} dev: true + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, tarball: https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, tarball: https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz} dependencies: @@ -3346,6 +3376,12 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} dev: false + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz} + dependencies: + dequal: 2.0.3 + dev: true + /array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==, tarball: https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz} engines: {node: '>= 0.4'} @@ -4252,6 +4288,11 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, tarball: https://registry.npmjs.org/depd/-/depd-2.0.0.tgz} engines: {node: '>= 0.8'} + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, tarball: https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz} + engines: {node: '>=6'} + dev: true + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, tarball: https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -4279,6 +4320,10 @@ packages: path-type: 4.0.0 dev: false + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, tarball: https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz} + dev: true + /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==, tarball: https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz} engines: {node: '>=12'} @@ -6524,6 +6569,11 @@ packages: yallist: 4.0.0 dev: false + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, tarball: https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz} + hasBin: true + dev: true + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==, tarball: https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz} engines: {node: '>=6'} @@ -7399,6 +7449,15 @@ packages: ansi-styles: 4.3.0 react-is: 17.0.2 + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, tarball: https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==, tarball: https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} diff --git a/svg/icon-carpet-roll.svg b/svg/icon-carpet-roll.svg new file mode 100644 index 0000000..90d57e3 --- /dev/null +++ b/svg/icon-carpet-roll.svg @@ -0,0 +1,54 @@ + + + + + + + + + +