@purpurds/icon 5.34.1 → 5.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purpurds/icon",
3
- "version": "5.34.1",
3
+ "version": "5.34.3",
4
4
  "license": "AGPL-3.0-only",
5
5
  "main": "./dist/icon.cjs.js",
6
6
  "types": "./dist/icon.d.ts",
@@ -17,13 +17,12 @@
17
17
  "source": "src/icon.tsx",
18
18
  "dependencies": {
19
19
  "classnames": "~2.5.0",
20
- "@purpurds/tokens": "5.34.1"
20
+ "@purpurds/tokens": "5.34.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@rushstack/eslint-patch": "~1.10.0",
24
24
  "@storybook/blocks": "^8.3.5",
25
25
  "@storybook/react": "^8.3.5",
26
- "storybook": "^8.3.5",
27
26
  "@telia/base-rig": "~8.2.0",
28
27
  "@telia/react-rig": "~3.2.0",
29
28
  "@testing-library/dom": "~9.3.3",
@@ -35,6 +34,7 @@
35
34
  "@types/react": "^18.3.3",
36
35
  "@types/styled-components": "^5.1.4",
37
36
  "change-case": "~4.1.2",
37
+ "dedent": "~1.5.1",
38
38
  "esbuild": "~0.14.23",
39
39
  "eslint-plugin-testing-library": "~6.2.0",
40
40
  "eslint": "^8.57.0",
@@ -45,14 +45,19 @@
45
45
  "prettier": "~2.8.8",
46
46
  "react-dom": "^18.3.1",
47
47
  "react": "^18.3.1",
48
+ "storybook": "^8.3.5",
48
49
  "svgo": "3.2.0",
50
+ "tsx": "~4.7.0",
49
51
  "typescript": "^5.6.3",
50
52
  "vite": "5.4.8",
51
- "vitest": "^2.1.2",
52
- "dedent": "~1.5.1",
53
53
  "vitest-axe": "~0.1.0",
54
- "tsx": "~4.7.0",
55
- "@purpurds/component-rig": "1.0.0"
54
+ "vitest": "^2.1.2",
55
+ "@purpurds/component-rig": "1.0.0",
56
+ "@purpurds/autocomplete": "5.34.3",
57
+ "@purpurds/heading": "5.34.3",
58
+ "@purpurds/paragraph": "5.34.3",
59
+ "@purpurds/text-field": "5.34.3",
60
+ "@purpurds/listbox": "5.34.3"
56
61
  },
57
62
  "scripts": {
58
63
  "build:dev": "vite",
@@ -0,0 +1,181 @@
1
+ @keyframes discoScale {
2
+ 0% {
3
+ transform: scale(1);
4
+ }
5
+ 50% {
6
+ transform: scale(1.3);
7
+ }
8
+ 100% {
9
+ transform: scale(1);
10
+ }
11
+ }
12
+
13
+ @keyframes discoSlide {
14
+ 0% {
15
+ transform: translateX(6px);
16
+ }
17
+ 50% {
18
+ transform: translatex(0);
19
+ }
20
+ 100% {
21
+ transform: translateX(-6px);
22
+ }
23
+ }
24
+
25
+ @keyframes discoWiggle {
26
+ 0% {
27
+ transform: rotate(12deg);
28
+ }
29
+ 50% {
30
+ transform: rotate(0);
31
+ }
32
+ 100% {
33
+ transform: rotate(-12deg);
34
+ }
35
+ }
36
+
37
+ @keyframes discoJump {
38
+ 0% {
39
+ transform: translateY(8px);
40
+ }
41
+ 50% {
42
+ transform: translateY(0);
43
+ }
44
+ 100% {
45
+ transform: translateY(-2px);
46
+ }
47
+ }
48
+
49
+ @property --rotate {
50
+ syntax: "<angle>";
51
+ initial-value: 0deg;
52
+ inherits: false;
53
+ }
54
+
55
+ @keyframes spin {
56
+ 0% {
57
+ --rotate: 0deg;
58
+ }
59
+ 100% {
60
+ --rotate: 360deg;
61
+ }
62
+ }
63
+
64
+ .wrapper,
65
+ .disco-wrapper {
66
+ padding: var(--purpur-spacing-600) var(--purpur-spacing-400);
67
+ border-radius: var(--purpur-border-radius-md);
68
+ }
69
+
70
+ .disco-wrapper {
71
+ position: relative;
72
+ background: black;
73
+ cursor: pointer;
74
+ }
75
+
76
+ .disco-wrapper::before {
77
+ content: "";
78
+ width: calc(100% + 8px);
79
+ height: calc(100% + 8px);
80
+ border-radius: calc(var(--purpur-border-radius-md) * 1.5);
81
+ background-image: linear-gradient(
82
+ var(--rotate),
83
+ red,
84
+ orange,
85
+ yellow,
86
+ green,
87
+ blue,
88
+ indigo,
89
+ violet
90
+ );
91
+ position: absolute;
92
+ z-index: -1;
93
+ top: -4px;
94
+ left: -4px;
95
+ animation: spin 8s linear infinite;
96
+ }
97
+
98
+ .disco-wrapper::after {
99
+ position: absolute;
100
+ content: "";
101
+ top: -15%;
102
+ left: -15%;
103
+ z-index: -1;
104
+ height: 130%;
105
+ width: 130%;
106
+ margin: 0 auto;
107
+ transform: scale(0.8);
108
+ filter: blur(16vh);
109
+ background-image: linear-gradient(
110
+ var(--rotate),
111
+ red,
112
+ orange,
113
+ yellow,
114
+ green,
115
+ blue,
116
+ indigo,
117
+ violet
118
+ );
119
+ opacity: 1;
120
+ transition: opacity 0.5s;
121
+ animation: spin 8s linear infinite;
122
+ }
123
+
124
+ .icon-grid,
125
+ .disco-icon-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
128
+ gap: 20px;
129
+ }
130
+
131
+ .disco-icon-grid {
132
+ gap: 30px;
133
+ margin-top: 48px;
134
+ }
135
+
136
+ .icon-wrapper {
137
+ display: flex;
138
+ flex-direction: row;
139
+ align-items: center;
140
+ gap: 12px;
141
+ }
142
+
143
+ .icon-box {
144
+ display: flex;
145
+ flex-shrink: 0;
146
+ padding: var(--purpur-spacing-50);
147
+ background-color: white;
148
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-medium);
149
+ border-radius: var(--purpur-border-radius-sm);
150
+ box-shadow: rgba(0, 0, 0, 0.1) 0 1px 3px 0;
151
+ }
152
+
153
+ .disco-icon-wrapper {
154
+ transform-origin: center;
155
+ display: flex;
156
+ flex-direction: row;
157
+ align-items: center;
158
+ justify-content: center;
159
+ gap: 12px;
160
+ }
161
+
162
+ .top-section {
163
+ display: flex;
164
+ justify-content: center;
165
+ align-items: center;
166
+ align-items: center;
167
+ gap: var(--purpur-spacing-250);
168
+ flex-direction: column;
169
+ }
170
+
171
+ .input-wrapper {
172
+ max-width: 300px;
173
+ width: 100%;
174
+ }
175
+
176
+ .disco-heading {
177
+ text-align: center;
178
+ margin-bottom: 24px;
179
+ cursor: pointer;
180
+ transition: color 200ms ease;
181
+ }
@@ -1,7 +1,16 @@
1
- import React from "react";
1
+ import { ReactNode, useLayoutEffect, useState } from "react";
2
+ import { Autocomplete } from "@purpurds/autocomplete";
3
+ import { Heading, HeadingProps } from "@purpurds/heading";
4
+ import { Paragraph } from "@purpurds/paragraph";
5
+ import { TextField } from "@purpurds/text-field";
2
6
  import type { Meta, StoryObj } from "@storybook/react";
3
7
 
4
- import { Heading } from "../../heading";
8
+ import "@purpurds/text-field/styles";
9
+ import "@purpurds/autocomplete/styles";
10
+ import "@purpurds/listbox/styles";
11
+ import "@purpurds/heading/styles";
12
+ import "@purpurds/tokens";
13
+ import "./icon.stories.css";
5
14
  import { Icon, sizes } from "./icon";
6
15
  import { iconList } from "./icon-imports";
7
16
 
@@ -71,61 +80,143 @@ const result = Object.entries(iconList).reduce((acc, cur) => {
71
80
  }, [] as unknown as Acc);
72
81
  const sortedResult = result.sort((a, b) => a.name.localeCompare(b.name));
73
82
 
74
- const Categories = () => (
75
- <>
76
- {sortedResult.map((z) => (
77
- <span key={z.name}>
78
- <Heading variant="title-100" tag="h2" style={{ margin: "24px 0" }}>
79
- {z.name.charAt(0).toUpperCase() + z.name.slice(1)}
80
- </Heading>
81
- <div
82
- style={{
83
- display: "grid",
84
- gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
85
- gap: 20,
86
- }}
87
- >
88
- {z.entries.map((icon) => (
89
- <div
90
- style={{
91
- display: "flex",
92
- flexDirection: "row",
93
- alignItems: "center",
94
- gap: 12,
95
- }}
96
- key={icon.name}
97
- >
98
- <div
99
- style={{
100
- display: "flex",
101
- flexShrink: 0,
102
- padding: 4,
103
- background: "#fff",
104
- borderRadius: 4,
105
- border: "1px solid hsla(203, 50%, 30%, 0.15)",
106
- boxShadow: "rgba(0, 0, 0, 0.10) 0 1px 3px 0",
107
- }}
108
- >
109
- <Icon svg={icon} size="lg" />
110
- </div>
111
- <p
112
- style={{
113
- margin: 0,
114
- color: "#2E3438",
115
- fontSize: 14,
116
- lineHeight: 1.2,
117
- fontFamily: "TeliaSans, Helvetica, Arial, 'Lucida Grande', sans-serif",
118
- }}
119
- >
120
- {icon.name}
121
- </p>
122
- </div>
83
+ const defaultAnimation = "discoScale 1.5s ease-in-out 500ms infinite";
84
+
85
+ const useDiscoProps = () => {
86
+ const [color, setColor] = useState("rgb(0, 0, 0)");
87
+ const [animation, setAnimation] = useState(defaultAnimation);
88
+
89
+ useLayoutEffect(() => {
90
+ const interval = setInterval(() => {
91
+ const r = Math.round(Math.random() * 255);
92
+ const g = Math.round(Math.random() * 255);
93
+ const b = Math.round(Math.random() * 255);
94
+ setColor(`rgb(${r}, ${g}, ${b})`);
95
+
96
+ if (animation === defaultAnimation) {
97
+ const animationNames = ["discoSlide", "discoScale", "discoWiggle", "discoJump"];
98
+ const animationName = animationNames[Math.floor(Math.random() * animationNames.length)];
99
+ const duration = Math.round(Math.random() * 200) + 200;
100
+ const delay = Math.round(Math.random() * 300);
101
+ setAnimation(`${animationName} ${duration}ms linear ${delay}ms infinite alternate`);
102
+ }
103
+ }, Math.round(Math.random() * 200) + 50);
104
+
105
+ return () => clearInterval(interval);
106
+ });
107
+ return { animation, color };
108
+ };
109
+
110
+ const DiscoIcon = ({ icon }: { icon: { name: string; svg: string } }) => {
111
+ const { color, animation } = useDiscoProps();
112
+
113
+ return (
114
+ <div className="disco-icon-wrapper" style={{ color, animation }}>
115
+ <Icon svg={icon} size="lg" />
116
+ <Paragraph style={{ color: color }}>{icon.name}</Paragraph>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ const DiscoHeading = ({ children, ...headingProps }: { children: ReactNode } & HeadingProps) => {
122
+ const { color, animation } = useDiscoProps();
123
+
124
+ return (
125
+ <Heading style={{ color, animation }} className="disco-heading" {...headingProps}>
126
+ {children}
127
+ </Heading>
128
+ );
129
+ };
130
+
131
+ const Categories = () => {
132
+ const options = sortedResult.flatMap(({ entries }) =>
133
+ entries.map(({ name }) => ({ id: name, label: name }))
134
+ );
135
+ const optionsWithDisco = [
136
+ ...options.slice(0, 3),
137
+ { id: "disco", label: "disco" },
138
+ ...options.slice(3),
139
+ ];
140
+ const [searchValue, setSearchValue] = useState("");
141
+
142
+ const disco = searchValue === "disco";
143
+ const filteredResult = sortedResult
144
+ .map(({ name, entries }) => ({
145
+ name: name,
146
+ entries: entries.filter(
147
+ ({ name, keywords }) =>
148
+ name.toLowerCase().includes(searchValue.toLowerCase()) ||
149
+ keywords.some((y) => y.toLowerCase().includes(searchValue.toLowerCase()))
150
+ ),
151
+ }))
152
+ .filter(({ entries }) => entries.length > 0);
153
+
154
+ const allIcons = sortedResult.flatMap(({ entries }) => entries);
155
+ const HeadingComponent = disco ? DiscoHeading : Heading;
156
+ const interactiveProps = {
157
+ role: "button",
158
+ onKeyUp: (e: React.KeyboardEvent<HTMLDivElement>) =>
159
+ e.key === "Escape" && disco && setSearchValue(""),
160
+ onClick: () => disco && setSearchValue(""),
161
+ tabIndex: 0,
162
+ };
163
+ return (
164
+ <div
165
+ id="icons-story-wrapper"
166
+ className={disco ? "disco-wrapper" : "wrapper"}
167
+ {...(disco && interactiveProps)}
168
+ >
169
+ <div className="top-section">
170
+ <HeadingComponent id="purpur-icons-heading" variant="display-25" tag="h2">
171
+ Purpur icons
172
+ </HeadingComponent>
173
+ <div className="input-wrapper" style={{ display: disco ? "none" : "initial" }}>
174
+ <Autocomplete
175
+ id="icon-search"
176
+ listboxLabel="icons"
177
+ options={optionsWithDisco}
178
+ onInputChange={setSearchValue}
179
+ inputValue={searchValue}
180
+ onSelect={(option) => {
181
+ option?.id === "disco" &&
182
+ setTimeout(() => {
183
+ document.getElementById("icons-story-wrapper")?.focus({ preventScroll: true });
184
+ }, 100);
185
+ }}
186
+ renderInput={(props) => (
187
+ <TextField {...props} type="text" id="icon-search" placeholder="Search..." />
188
+ )}
189
+ />
190
+ </div>
191
+ </div>
192
+ {disco ? (
193
+ <div className="disco-icon-grid">
194
+ {allIcons.map((icon) => (
195
+ <DiscoIcon icon={icon} key={icon.name} />
123
196
  ))}
124
197
  </div>
125
- </span>
126
- ))}
127
- </>
128
- );
198
+ ) : (
199
+ filteredResult.map((z) => (
200
+ <span key={z.name}>
201
+ <Heading variant="title-100" tag="h2" style={{ margin: "24px 0" }}>
202
+ {z.name.charAt(0).toUpperCase() + z.name.slice(1)}
203
+ </Heading>
204
+ <div className="icon-grid">
205
+ {z.entries.map((icon) => (
206
+ <div className="icon-wrapper" key={icon.name}>
207
+ <div className="icon-box">
208
+ <Icon svg={icon} size="lg" />
209
+ </div>
210
+ <Paragraph>{icon.name}</Paragraph>
211
+ </div>
212
+ ))}
213
+ </div>
214
+ </span>
215
+ ))
216
+ )}
217
+ </div>
218
+ );
219
+ };
129
220
 
130
221
  export const AllIcons: Story = {
131
222
  parameters: {