sid-ui 0.1.2 → 0.2.0

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": "sid-ui",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "packageManager": "bun@1.4.2",
5
5
  "description": "React components, tokens, and theme used by Joined products.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useState, type CSSProperties } from "react";
4
4
 
5
5
  import { segmentShares } from "./chartMath";
6
6
  import { ChartTable, ChartTip, defaultFormat, type ChartTone } from "./chartParts";
@@ -84,7 +84,7 @@ export function BarChart({
84
84
  <span
85
85
  className="os-bars-mark"
86
86
  data-tone={datum.tone ?? tone}
87
- style={{ height: size(datum.value) }}
87
+ style={{ height: size(datum.value), "--os-bar-i": index } as CSSProperties}
88
88
  />
89
89
  {active === index ? tip(index) : null}
90
90
  </div>
@@ -107,7 +107,7 @@ export function BarChart({
107
107
  <span
108
108
  className="os-bars-mark"
109
109
  data-tone={datum.tone ?? tone}
110
- style={{ width: size(datum.value) }}
110
+ style={{ width: size(datum.value), "--os-bar-i": index } as CSSProperties}
111
111
  />
112
112
  {active === index ? tip(index) : null}
113
113
  </span>
@@ -0,0 +1,199 @@
1
+ "use client";
2
+
3
+ import {
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type PointerEvent,
9
+ type ReactNode,
10
+ } from "react";
11
+
12
+ import { Glyph } from "./Glyph";
13
+
14
+ /** lift rises on hover; tilt leans toward the pointer with a glare; spotlight glows under it. */
15
+ export type MotionCardEffect = "lift" | "tilt" | "spotlight";
16
+
17
+ export interface MotionCardProps {
18
+ children: ReactNode;
19
+ effect?: MotionCardEffect;
20
+ /** Inner padding as any CSS length. */
21
+ padding?: string;
22
+ /** Width as a number of px or any CSS width. */
23
+ width?: number | string;
24
+ /** Tilt only: the largest lean in degrees. */
25
+ maxTilt?: number;
26
+ /** Spotlight and tilt: the glow color. Defaults to the accent. */
27
+ glowColor?: string;
28
+ className?: string;
29
+ }
30
+
31
+ function size(value: number | string | undefined) {
32
+ return typeof value === "number" ? `${value}px` : value;
33
+ }
34
+
35
+ /**
36
+ * A card that answers the pointer. The effect is pure CSS driven by custom properties, so moving
37
+ * the pointer never re-renders React. Reduced motion keeps the glow and drops the movement.
38
+ */
39
+ export function MotionCard({
40
+ children,
41
+ effect = "lift",
42
+ padding = "var(--spacing-5)",
43
+ width,
44
+ maxTilt = 10,
45
+ glowColor,
46
+ className,
47
+ }: MotionCardProps) {
48
+ const ref = useRef<HTMLDivElement>(null);
49
+
50
+ const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {
51
+ const node = ref.current;
52
+ if (!node || effect === "lift") return;
53
+ const box = node.getBoundingClientRect();
54
+ const x = (event.clientX - box.left) / box.width;
55
+ const y = (event.clientY - box.top) / box.height;
56
+ node.style.setProperty("--os-mc-x", `${x * 100}%`);
57
+ node.style.setProperty("--os-mc-y", `${y * 100}%`);
58
+ if (effect === "tilt") {
59
+ node.style.setProperty("--os-mc-rx", `${(0.5 - y) * maxTilt * 2}deg`);
60
+ node.style.setProperty("--os-mc-ry", `${(x - 0.5) * maxTilt * 2}deg`);
61
+ }
62
+ };
63
+
64
+ const onPointerLeave = () => {
65
+ const node = ref.current;
66
+ if (!node) return;
67
+ node.style.setProperty("--os-mc-rx", "0deg");
68
+ node.style.setProperty("--os-mc-ry", "0deg");
69
+ };
70
+
71
+ return (
72
+ <div
73
+ ref={ref}
74
+ className={["os-mc", `os-mc-${effect}`, className].filter(Boolean).join(" ")}
75
+ style={
76
+ {
77
+ "--os-mc-pad": padding,
78
+ "--os-mc-glow": glowColor,
79
+ width: size(width),
80
+ } as CSSProperties
81
+ }
82
+ onPointerMove={onPointerMove}
83
+ onPointerLeave={onPointerLeave}
84
+ >
85
+ <div className="os-mc-body">{children}</div>
86
+ {effect !== "lift" && <span className="os-mc-light" aria-hidden />}
87
+ </div>
88
+ );
89
+ }
90
+
91
+ export interface FlipCardProps {
92
+ front: ReactNode;
93
+ back: ReactNode;
94
+ /** Controlled side. Omit to let the card flip itself. */
95
+ isFlipped?: boolean;
96
+ onFlipChange?: (isFlipped: boolean) => void;
97
+ /** Both faces share this height. */
98
+ height: number | string;
99
+ width?: number | string;
100
+ /** Names the flip button for assistive tech. */
101
+ label?: string;
102
+ /** vertical flips over the top edge instead of the side. */
103
+ direction?: "horizontal" | "vertical";
104
+ }
105
+
106
+ /**
107
+ * Two faces on one card. The round button in the corner turns it over (or flip it from outside
108
+ * with `isFlipped`); the hidden face is inert so focus and screen readers stay on the visible side.
109
+ */
110
+ export function FlipCard({
111
+ front,
112
+ back,
113
+ isFlipped,
114
+ onFlipChange,
115
+ height,
116
+ width,
117
+ label = "Flip card",
118
+ direction = "horizontal",
119
+ }: FlipCardProps) {
120
+ const [own, setOwn] = useState(false);
121
+ const flipped = isFlipped ?? own;
122
+ const flip = () => {
123
+ if (isFlipped === undefined) setOwn(!flipped);
124
+ onFlipChange?.(!flipped);
125
+ };
126
+
127
+ return (
128
+ <div
129
+ className={["os-flip", `os-flip-${direction}`, flipped && "os-flip-on"].filter(Boolean).join(" ")}
130
+ style={{ height: size(height), width: size(width) }}
131
+ >
132
+ <div className="os-flip-inner">
133
+ <div className="os-flip-face os-flip-front" inert={flipped}>
134
+ {front}
135
+ </div>
136
+ <div className="os-flip-face os-flip-back" inert={!flipped}>
137
+ {back}
138
+ </div>
139
+ </div>
140
+ <button
141
+ type="button"
142
+ className="os-flip-toggle"
143
+ aria-label={label}
144
+ aria-pressed={flipped}
145
+ onClick={flip}
146
+ >
147
+ <Glyph name="refresh" />
148
+ </button>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ export interface ExpandableCardProps {
154
+ /** Always visible; clicking it toggles the body. */
155
+ header: ReactNode;
156
+ children: ReactNode;
157
+ defaultExpanded?: boolean;
158
+ isExpanded?: boolean;
159
+ onExpandedChange?: (isExpanded: boolean) => void;
160
+ width?: number | string;
161
+ }
162
+
163
+ /** A card whose body slides open under its header — FAQs, job details, order lines. */
164
+ export function ExpandableCard({
165
+ header,
166
+ children,
167
+ defaultExpanded = false,
168
+ isExpanded,
169
+ onExpandedChange,
170
+ width,
171
+ }: ExpandableCardProps) {
172
+ const id = useId();
173
+ const [own, setOwn] = useState(defaultExpanded);
174
+ const open = isExpanded ?? own;
175
+ const toggle = () => {
176
+ if (isExpanded === undefined) setOwn(!open);
177
+ onExpandedChange?.(!open);
178
+ };
179
+
180
+ return (
181
+ <div className={open ? "os-xcard os-xcard-open" : "os-xcard"} style={{ width: size(width) }}>
182
+ <button
183
+ type="button"
184
+ className="os-xcard-head"
185
+ aria-expanded={open}
186
+ aria-controls={id}
187
+ onClick={toggle}
188
+ >
189
+ <span className="os-xcard-title">{header}</span>
190
+ <span className="os-xcard-chevron" aria-hidden>
191
+ <Glyph name="chevronDown" />
192
+ </span>
193
+ </button>
194
+ <div className="os-xcard-panel" id={id} inert={!open}>
195
+ <div className="os-xcard-body">{children}</div>
196
+ </div>
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,250 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ type KeyboardEvent,
8
+ type ReactNode,
9
+ } from "react";
10
+
11
+ import { Glyph } from "./Glyph";
12
+
13
+ export type ChipColor =
14
+ | "neutral"
15
+ | "blue"
16
+ | "green"
17
+ | "yellow"
18
+ | "red"
19
+ | "purple"
20
+ | "pink"
21
+ | "teal"
22
+ | "orange"
23
+ | "cyan";
24
+
25
+ /** filled is a solid tint, soft a lighter one, outlined a hairline on the surface. */
26
+ export type ChipVariant = "filled" | "soft" | "outlined";
27
+
28
+ export type ChipSize = "sm" | "md" | "lg";
29
+
30
+ export interface ChipProps {
31
+ label: ReactNode;
32
+ /** Identifies the chip inside a ChipGroup. */
33
+ value?: string;
34
+ variant?: ChipVariant;
35
+ color?: ChipColor;
36
+ size?: ChipSize;
37
+ /** Leading visual — an Icon, a small Avatar, or a color swatch. */
38
+ start?: ReactNode;
39
+ /** A trailing count, such as the number of results behind a filter. */
40
+ count?: number;
41
+ /** Selected filter chips take the accent and grow a check mark. */
42
+ isSelected?: boolean;
43
+ isDisabled?: boolean;
44
+ /** Makes the chip a button. Inside a ChipGroup the group handles clicks. */
45
+ onClick?: () => void;
46
+ /** Makes the chip a link. */
47
+ href?: string;
48
+ /** Adds a remove button. The chip collapses away first, then this runs. */
49
+ onRemove?: () => void;
50
+ removeLabel?: string;
51
+ }
52
+
53
+ type GroupContext = {
54
+ selected: string[];
55
+ toggle: (value: string) => void;
56
+ /** Unset for a plain group that only shares size and variant. */
57
+ mode?: "single" | "multiple";
58
+ size?: ChipSize;
59
+ variant?: ChipVariant;
60
+ };
61
+
62
+ const ChipGroupContext = createContext<GroupContext | null>(null);
63
+
64
+ /** Matches the collapse in chip.css, so the chip is gone before the parent drops it. */
65
+ const REMOVE_MS = 180;
66
+
67
+ function reducedMotion() {
68
+ return (
69
+ typeof window !== "undefined" &&
70
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
71
+ );
72
+ }
73
+
74
+ /**
75
+ * A compact, rounded label: a tag, a filter, a removable input, or a small action. Put several
76
+ * in a ChipGroup to make them a single or multiple choice.
77
+ */
78
+ export function Chip({
79
+ label,
80
+ value,
81
+ variant,
82
+ color = "neutral",
83
+ size,
84
+ start,
85
+ count,
86
+ isSelected,
87
+ isDisabled = false,
88
+ onClick,
89
+ href,
90
+ onRemove,
91
+ removeLabel = "Remove",
92
+ }: ChipProps) {
93
+ const group = useContext(ChipGroupContext);
94
+ const [leaving, setLeaving] = useState(false);
95
+ const inGroup = group?.mode != null && value != null;
96
+ const selected = inGroup ? (group?.selected.includes(value) ?? false) : Boolean(isSelected);
97
+ const resolvedSize = size ?? group?.size ?? "md";
98
+ const resolvedVariant = variant ?? group?.variant ?? "soft";
99
+ const selectable = inGroup || isSelected !== undefined;
100
+
101
+ const remove = () => {
102
+ if (!onRemove || leaving) return;
103
+ if (reducedMotion()) {
104
+ onRemove();
105
+ return;
106
+ }
107
+ setLeaving(true);
108
+ window.setTimeout(onRemove, REMOVE_MS);
109
+ };
110
+
111
+ const content = (
112
+ <>
113
+ {selectable && (
114
+ <span className="os-chip-check" aria-hidden>
115
+ <span>
116
+ <Glyph name="check" />
117
+ </span>
118
+ </span>
119
+ )}
120
+ {start != null && <span className="os-chip-start">{start}</span>}
121
+ <span className="os-chip-label">{label}</span>
122
+ {count != null && <span className="os-chip-count">{count}</span>}
123
+ </>
124
+ );
125
+
126
+ const className = [
127
+ "os-chip",
128
+ `os-chip-${resolvedVariant}`,
129
+ `os-chip-${color}`,
130
+ `os-chip-${resolvedSize}`,
131
+ selected && "os-chip-selected",
132
+ isDisabled && "os-chip-disabled",
133
+ onRemove && "os-chip-removable",
134
+ leaving && "os-chip-leaving",
135
+ ]
136
+ .filter(Boolean)
137
+ .join(" ");
138
+
139
+ const handleClick = () => {
140
+ if (inGroup) group?.toggle(value);
141
+ onClick?.();
142
+ };
143
+
144
+ let main: ReactNode;
145
+ if (href && !isDisabled) {
146
+ main = (
147
+ <a className="os-chip-main" href={href}>
148
+ {content}
149
+ </a>
150
+ );
151
+ } else if (inGroup || onClick || selectable) {
152
+ const single = inGroup && group?.mode === "single";
153
+ main = (
154
+ <button
155
+ type="button"
156
+ className="os-chip-main"
157
+ onClick={handleClick}
158
+ disabled={isDisabled}
159
+ role={single ? "radio" : undefined}
160
+ aria-checked={single ? selected : undefined}
161
+ aria-pressed={!single && selectable ? selected : undefined}
162
+ >
163
+ {content}
164
+ </button>
165
+ );
166
+ } else {
167
+ main = <span className="os-chip-main">{content}</span>;
168
+ }
169
+
170
+ const onKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
171
+ if (onRemove && (event.key === "Backspace" || event.key === "Delete")) {
172
+ event.preventDefault();
173
+ remove();
174
+ }
175
+ };
176
+
177
+ return (
178
+ <span className={className} onKeyDown={onRemove ? onKeyDown : undefined}>
179
+ <span className="os-chip-inner">
180
+ {main}
181
+ {onRemove && (
182
+ <button
183
+ type="button"
184
+ className="os-chip-remove"
185
+ aria-label={typeof label === "string" ? `${removeLabel} ${label}` : removeLabel}
186
+ onClick={remove}
187
+ disabled={isDisabled}
188
+ >
189
+ <Glyph name="close" />
190
+ </button>
191
+ )}
192
+ </span>
193
+ </span>
194
+ );
195
+ }
196
+
197
+ export interface ChipGroupProps {
198
+ children: ReactNode;
199
+ /** Accessible name for the set. */
200
+ label: string;
201
+ /** single behaves like radio buttons, multiple like toggles. Omit for a plain set of chips. */
202
+ selectionMode?: "single" | "multiple";
203
+ /** Selected chip values. */
204
+ value?: string[];
205
+ onChange?: (value: string[]) => void;
206
+ /** single mode: allow clicking the selected chip to clear it. */
207
+ isClearable?: boolean;
208
+ size?: ChipSize;
209
+ variant?: ChipVariant;
210
+ /** Lays the chips in one scrolling row instead of wrapping. */
211
+ isScrollable?: boolean;
212
+ }
213
+
214
+ /** A row of chips that share a size and, optionally, a selection. */
215
+ export function ChipGroup({
216
+ children,
217
+ label,
218
+ selectionMode,
219
+ value = [],
220
+ onChange,
221
+ isClearable = false,
222
+ size,
223
+ variant,
224
+ isScrollable = false,
225
+ }: ChipGroupProps) {
226
+ const toggle = (chip: string) => {
227
+ if (!onChange) return;
228
+ if (selectionMode === "single") {
229
+ const isOn = value.includes(chip);
230
+ onChange(isOn ? (isClearable ? [] : value) : [chip]);
231
+ } else {
232
+ onChange(value.includes(chip) ? value.filter((v) => v !== chip) : [...value, chip]);
233
+ }
234
+ };
235
+
236
+ const role = selectionMode === "single" ? "radiogroup" : "group";
237
+ const groupClass = ["os-chip-group", isScrollable && "os-chip-group-scroll"]
238
+ .filter(Boolean)
239
+ .join(" ");
240
+
241
+ return (
242
+ <ChipGroupContext.Provider
243
+ value={{ selected: value, toggle, mode: selectionMode, size, variant }}
244
+ >
245
+ <div className={groupClass} role={role} aria-label={label}>
246
+ {children}
247
+ </div>
248
+ </ChipGroupContext.Provider>
249
+ );
250
+ }
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
+ useEffect,
4
5
  useId,
5
6
  useRef,
7
+ useState,
6
8
  type ClipboardEvent,
7
9
  type ChangeEvent,
8
10
  type CSSProperties,
@@ -41,7 +43,74 @@ export interface CodeInputProps {
41
43
  size?: ControlSize;
42
44
  }
43
45
 
44
- /** One round cell per character. Typing, pasting, backspace, and arrow keys move between cells. */
46
+ const DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
47
+
48
+ /** Base spin time plus a little per digit travelled, so 0→9 takes longer than 0→1. */
49
+ const ROLL_BASE_MS = 520;
50
+ const ROLL_PER_STEP_MS = 55;
51
+
52
+ /**
53
+ * The visible face of a cell. A digit is a drum of 0–9 that spins from where it was (0 for a new
54
+ * digit) to the typed one and settles with a little overshoot; a letter drops in.
55
+ */
56
+ function CodeFace({ character, delay }: { character: string; delay: number }) {
57
+ const digit = /^[0-9]$/.test(character) ? Number(character) : null;
58
+ const [position, setPosition] = useState(0);
59
+ const [duration, setDuration] = useState(0);
60
+ const from = useRef(0);
61
+
62
+ useEffect(() => {
63
+ if (digit === null) return;
64
+ // Two frames: the drum paints at its start before the transition runs.
65
+ let second = 0;
66
+ const first = requestAnimationFrame(() => {
67
+ second = requestAnimationFrame(() => {
68
+ setDuration(ROLL_BASE_MS + Math.abs(digit - from.current) * ROLL_PER_STEP_MS);
69
+ setPosition(digit);
70
+ from.current = digit;
71
+ });
72
+ });
73
+ return () => {
74
+ cancelAnimationFrame(first);
75
+ cancelAnimationFrame(second);
76
+ };
77
+ }, [digit]);
78
+
79
+ if (!character) return null;
80
+
81
+ if (digit === null) {
82
+ return (
83
+ <span className="os-code-face" aria-hidden>
84
+ <span className="os-code-letter" style={{ animationDelay: `${delay}ms` }}>
85
+ {character}
86
+ </span>
87
+ </span>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <span className="os-code-face os-code-face-drum" aria-hidden>
93
+ <span
94
+ className="os-code-drum"
95
+ style={
96
+ {
97
+ "--os-code-roll": position,
98
+ transitionDuration: `${duration}ms`,
99
+ transitionDelay: `${delay}ms`,
100
+ } as CSSProperties
101
+ }
102
+ >
103
+ {DIGITS.map((d) => (
104
+ <span key={d} className="os-code-digit">
105
+ {d}
106
+ </span>
107
+ ))}
108
+ </span>
109
+ </span>
110
+ );
111
+ }
112
+
113
+ /** One cell per character. Typing, pasting, backspace, and arrow keys move between cells. */
45
114
  export function CodeInput({
46
115
  value,
47
116
  onChange,
@@ -61,6 +130,14 @@ export function CodeInput({
61
130
  // Focus moves before React re-renders, so handlers read the newest value from here.
62
131
  const latest = useRef(value);
63
132
  latest.current = value;
133
+ // When several cells change at once (a paste, a reset), their drums start one after another.
134
+ const [seen, setSeen] = useState(value);
135
+ const [isBatch, setIsBatch] = useState(false);
136
+ if (seen !== value) {
137
+ const changed = Array.from({ length }, (_, i) => seen[i] !== value[i]).filter(Boolean).length;
138
+ setSeen(value);
139
+ setIsBatch(changed > 1);
140
+ }
64
141
 
65
142
  const focusCell = (index: number) => {
66
143
  const cell = cells.current[Math.max(0, Math.min(index, length - 1))];
@@ -146,6 +223,11 @@ export function CodeInput({
146
223
  data-filled={character ? "true" : "false"}
147
224
  style={{ "--os-code-index": index } as CSSProperties}
148
225
  >
226
+ <CodeFace
227
+ key={character ? "filled" : "empty"}
228
+ character={character}
229
+ delay={isBatch ? index * 70 : 0}
230
+ />
149
231
  <input
150
232
  ref={(node) => {
151
233
  cells.current[index] = node;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useId, useState } from "react";
4
4
 
5
5
  import { arcPath, segmentShares } from "./chartMath";
6
6
  import { ChartTable, defaultFormat, toneAt, type ChartTone } from "./chartParts";
@@ -36,6 +36,7 @@ export function DonutChart({
36
36
  formatValue?: (value: number) => string;
37
37
  }) {
38
38
  const [active, setActive] = useState<number | null>(null);
39
+ const maskId = `os-donut-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
39
40
  const shares = segmentShares(data.map((slice) => slice.value));
40
41
  const total = data.reduce((sum, slice) => sum + Math.max(0, slice.value), 0);
41
42
  const outer = size / 2;
@@ -62,7 +63,20 @@ export function DonutChart({
62
63
  return (
63
64
  <div className="os-donut">
64
65
  <div className="os-donut-figure" style={{ width: size, height: size }}>
65
- <svg width={size} height={size} aria-hidden>
66
+ <svg width={size} height={size} aria-hidden overflow="visible">
67
+ <defs>
68
+ <mask id={maskId}>
69
+ <circle
70
+ className="os-donut-sweep"
71
+ cx={outer}
72
+ cy={outer}
73
+ r={outer / 2 + inner / 2}
74
+ strokeWidth={outer - inner + 12}
75
+ pathLength={1}
76
+ transform={`rotate(-90 ${outer} ${outer})`}
77
+ />
78
+ </mask>
79
+ </defs>
66
80
  <circle
67
81
  className="os-donut-track"
68
82
  cx={outer}
@@ -70,19 +84,22 @@ export function DonutChart({
70
84
  r={(outer + inner) / 2}
71
85
  strokeWidth={outer - inner}
72
86
  />
73
- {arcs.map(({ slice, index, d }) =>
74
- d ? (
75
- <path
76
- key={slice.label}
77
- d={d}
78
- className="os-donut-slice"
79
- data-tone={slice.tone}
80
- data-dim={active !== null && active !== index ? true : undefined}
81
- onPointerEnter={() => setActive(index)}
82
- onPointerLeave={() => setActive(null)}
83
- />
84
- ) : null,
85
- )}
87
+ <g mask={`url(#${maskId})`}>
88
+ {arcs.map(({ slice, index, d }) =>
89
+ d ? (
90
+ <path
91
+ key={slice.label}
92
+ d={d}
93
+ className="os-donut-slice"
94
+ data-tone={slice.tone}
95
+ data-dim={active !== null && active !== index ? true : undefined}
96
+ data-active={active === index || undefined}
97
+ onPointerEnter={() => setActive(index)}
98
+ onPointerLeave={() => setActive(null)}
99
+ />
100
+ ) : null,
101
+ )}
102
+ </g>
86
103
  </svg>
87
104
  <div className="os-donut-center" aria-live="polite">
88
105
  <strong>{formatValue(focus ? focus.value : total)}</strong>
@@ -224,6 +224,16 @@ const MARKS = {
224
224
  d: "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",
225
225
  },
226
226
  ],
227
+ alert: [
228
+ { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" },
229
+ { d: "M12 9v4" },
230
+ { d: "M12 17h.01" },
231
+ ],
232
+ alertCircle: [
233
+ { cx: "12", cy: "12", r: "10" },
234
+ { x1: "12", y1: "8", x2: "12", y2: "12" },
235
+ { x1: "12", y1: "16", x2: "12.01", y2: "16" },
236
+ ],
227
237
  info: [{ cx: "12", cy: "12", r: "10" }, { d: "M12 16v-4" }, { d: "M12 8h.01" }],
228
238
  panelRight: [{ x: "3", y: "3", width: "18", height: "18", rx: "2" }, { d: "M15 3v18" }],
229
239
  creditCard: [{ x: "2", y: "5", width: "20", height: "14", rx: "2" }, { d: "M2 10h20" }],