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 0000000..9f3c34a Binary files /dev/null and b/assets/images/icons/carpet-roll-64.png differ 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 @@ + + + + + + + + + +