@hyperframes/studio 0.8.58 → 0.8.60
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/dist/assets/{hyperframes-player-zGyOcwwh.js → hyperframes-player-Di7GyCOP.js} +3 -3
- package/dist/assets/{index-DKJpHYYd.css → index-CBKb_0TI.css} +1 -1
- package/dist/assets/{index-d8ferjRc.js → index-Dpq2lkOm.js} +190 -190
- package/dist/assets/{index-ZJaRCDYV.js → index-Dx_pt9m4.js} +1 -1
- package/dist/assets/{index-BcE2OSVA.js → index-jAjhQTrB.js} +1 -1
- package/dist/index.d.ts +549 -13
- package/dist/index.html +2 -2
- package/dist/index.js +23133 -21941
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/StudioHeader.tsx +2 -13
- package/src/components/TimelineToolbar.test.tsx +42 -0
- package/src/components/TimelineToolbar.tsx +22 -8
- package/src/components/editor/audioInspectorInput.test.ts +55 -0
- package/src/components/editor/audioInspectorInput.ts +55 -0
- package/src/components/editor/propertyPanelFill.tsx +3 -2
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +172 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +87 -1
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +92 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +13 -9
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +106 -0
- package/src/components/editor/useColorGradingController.ts +2 -6
- package/src/components/icons/AudioMetersIcon.tsx +16 -0
- package/src/components/icons/InspectorIcon.tsx +18 -0
- package/src/components/icons/ReverseGradientIcon.tsx +23 -0
- package/src/components/icons/RippleEditIcon.tsx +19 -0
- package/src/components/nle/AudioMeterStrip.test.tsx +395 -0
- package/src/components/nle/AudioMeterStrip.tsx +430 -0
- package/src/components/nle/TimelinePane.tsx +29 -23
- package/src/components/ui/Menu.test.tsx +3 -4
- package/src/components/ui/Menu.tsx +18 -5
- package/src/components/ui/Select.tsx +7 -1
- package/src/components/ui/Tooltip.tsx +6 -1
- package/src/contexts/TimelineEditContext.tsx +4 -0
- package/src/hooks/useTimelineAssetDropOps.ts +9 -1
- package/src/index.ts +16 -0
- package/src/player/components/AudioWaveform.tsx +16 -7
- package/src/player/components/ImageThumbnail.tsx +3 -5
- package/src/player/components/LayerDisclosureRow.tsx +9 -4
- package/src/player/components/PlayheadIndicator.tsx +3 -3
- package/src/player/components/Timeline.test.ts +22 -31
- package/src/player/components/Timeline.tsx +34 -7
- package/src/player/components/TimelineAutomationLane.test.tsx +1 -1
- package/src/player/components/TimelineAutomationLane.tsx +5 -5
- package/src/player/components/TimelineCanvas.tsx +28 -71
- package/src/player/components/TimelineClip.test.tsx +41 -4
- package/src/player/components/TimelineClip.tsx +25 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.tsx +2 -2
- package/src/player/components/TimelineClipFades.test.tsx +171 -0
- package/src/player/components/TimelineClipFades.tsx +302 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +1 -1
- package/src/player/components/TimelineFxButton.tsx +10 -8
- package/src/player/components/TimelineGroupHeader.tsx +18 -9
- package/src/player/components/TimelineGroupLaneLabels.tsx +12 -5
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +14 -0
- package/src/player/components/TimelineOverlays.tsx +164 -165
- package/src/player/components/TimelineParts.test.tsx +101 -0
- package/src/player/components/TimelineParts.tsx +74 -0
- package/src/player/components/TimelinePartsParity.test.tsx +58 -0
- package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
- package/src/player/components/TimelineProvider.tsx +25 -0
- package/src/player/components/TimelineRazorInteraction.tsx +2 -2
- package/src/player/components/TimelineRuler.tsx +3 -2
- package/src/player/components/TimelineRulerPart.tsx +22 -0
- package/src/player/components/TimelineShortcutHint.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +4 -6
- package/src/player/components/TimelineTrackPlainHeader.tsx +10 -3
- package/src/player/components/VideoThumbnail.tsx +3 -5
- package/src/player/components/timelineMotionStyles.test.ts +96 -30
- package/src/player/components/timelineProviderStateBuilders.ts +20 -0
- package/src/player/components/timelineTheme.test.ts +14 -0
- package/src/player/components/timelineTheme.ts +12 -11
- package/src/player/components/useTimelineProviderState.tsx +19 -12
- package/src/player/lib/timelineElementHelpers.ts +20 -0
- package/src/player/store/timelineElement.ts +5 -0
- package/src/styles/hex-baseline.json +4 -23
- package/src/styles/studio.css +43 -47
- package/src/styles/theme.css +109 -1
- package/src/styles/theme.test.ts +9 -0
- package/src/utils/audioMeterMath.test.ts +44 -0
- package/src/utils/audioMeterMath.ts +79 -0
- package/src/utils/audioMeterVisibility.ts +14 -0
- package/src/utils/studioHelpers.ts +21 -0
- package/src/utils/studioUiPreferences.test.ts +10 -0
- package/src/utils/studioUiPreferences.ts +5 -0
- package/src/utils/timelineAssetDrop.test.ts +32 -0
- package/src/utils/timelineAssetDrop.ts +6 -1
|
@@ -1285,3 +1285,95 @@ describe("FlatSelectRow — label/value options", () => {
|
|
|
1285
1285
|
act(() => root.unmount());
|
|
1286
1286
|
});
|
|
1287
1287
|
});
|
|
1288
|
+
|
|
1289
|
+
describe("FlatSlider — typed value", () => {
|
|
1290
|
+
it("keeps the readout inert without onCommitText", () => {
|
|
1291
|
+
const { host, root } = renderInto(
|
|
1292
|
+
<FlatSlider
|
|
1293
|
+
label="Opacity"
|
|
1294
|
+
value={10}
|
|
1295
|
+
min={0}
|
|
1296
|
+
max={100}
|
|
1297
|
+
tier="default"
|
|
1298
|
+
displayValue="10%"
|
|
1299
|
+
onCommit={vi.fn()}
|
|
1300
|
+
/>,
|
|
1301
|
+
);
|
|
1302
|
+
const readout = host.querySelector<HTMLElement>('[data-flat-slider-value="true"]');
|
|
1303
|
+
expect(readout?.getAttribute("role")).toBeNull();
|
|
1304
|
+
act(() => readout?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
1305
|
+
expect(host.querySelector('[data-flat-slider-input="true"]')).toBeNull();
|
|
1306
|
+
act(() => root.unmount());
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it("turns the readout into an input on click and commits the typed text on Enter", () => {
|
|
1310
|
+
const onCommitText = vi.fn().mockReturnValue(true);
|
|
1311
|
+
const { host, root } = renderInto(
|
|
1312
|
+
<FlatSlider
|
|
1313
|
+
label="Volume"
|
|
1314
|
+
value={0}
|
|
1315
|
+
min={-100}
|
|
1316
|
+
max={100}
|
|
1317
|
+
tier="default"
|
|
1318
|
+
displayValue="0 dB"
|
|
1319
|
+
onCommit={vi.fn()}
|
|
1320
|
+
onCommitText={onCommitText}
|
|
1321
|
+
/>,
|
|
1322
|
+
);
|
|
1323
|
+
const readout = host.querySelector<HTMLElement>('[data-flat-slider-value="true"]');
|
|
1324
|
+
expect(readout?.getAttribute("role")).toBe("button");
|
|
1325
|
+
act(() => readout?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
1326
|
+
const input = host.querySelector<HTMLInputElement>('[data-flat-slider-input="true"]');
|
|
1327
|
+
if (!input) throw new Error("expected the readout to become an input");
|
|
1328
|
+
expect(input.value).toBe("0 dB");
|
|
1329
|
+
act(() => {
|
|
1330
|
+
setReactInputValue(input, "-6 dB");
|
|
1331
|
+
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
1332
|
+
});
|
|
1333
|
+
expect(onCommitText).toHaveBeenCalledWith("-6 dB");
|
|
1334
|
+
expect(host.querySelector('[data-flat-slider-input="true"]')).toBeNull();
|
|
1335
|
+
act(() => root.unmount());
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it("stays open and marks itself invalid when the text is refused, and closes on Escape", () => {
|
|
1339
|
+
const onCommitText = vi.fn().mockReturnValue(false);
|
|
1340
|
+
const { host, root } = renderInto(
|
|
1341
|
+
<FlatSlider
|
|
1342
|
+
label="Speed"
|
|
1343
|
+
value={500}
|
|
1344
|
+
min={0}
|
|
1345
|
+
max={1000}
|
|
1346
|
+
tier="default"
|
|
1347
|
+
displayValue="1x"
|
|
1348
|
+
onCommit={vi.fn()}
|
|
1349
|
+
onCommitText={onCommitText}
|
|
1350
|
+
/>,
|
|
1351
|
+
);
|
|
1352
|
+
act(() =>
|
|
1353
|
+
host
|
|
1354
|
+
.querySelector<HTMLElement>('[data-flat-slider-value="true"]')
|
|
1355
|
+
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
1356
|
+
);
|
|
1357
|
+
const input = host.querySelector<HTMLInputElement>('[data-flat-slider-input="true"]');
|
|
1358
|
+
if (!input) throw new Error("expected an input");
|
|
1359
|
+
act(() => {
|
|
1360
|
+
setReactInputValue(input, "fast");
|
|
1361
|
+
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
1362
|
+
});
|
|
1363
|
+
expect(onCommitText).toHaveBeenCalledWith("fast");
|
|
1364
|
+
const still = host.querySelector<HTMLInputElement>('[data-flat-slider-input="true"]');
|
|
1365
|
+
expect(still).not.toBeNull();
|
|
1366
|
+
expect(still?.getAttribute("aria-invalid")).toBe("true");
|
|
1367
|
+
act(() => still?.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })));
|
|
1368
|
+
expect(host.querySelector('[data-flat-slider-input="true"]')).toBeNull();
|
|
1369
|
+
expect(host.querySelector('[data-flat-slider-value="true"]')?.textContent).toBe("1x");
|
|
1370
|
+
act(() => root.unmount());
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
/** Drives React's controlled input the way a keystroke would. */
|
|
1375
|
+
function setReactInputValue(input: HTMLInputElement, value: string) {
|
|
1376
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
1377
|
+
setter?.call(input, value);
|
|
1378
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1379
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useRef, useState
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
3
3
|
import { RotateCcw } from "../../icons/SystemIcons";
|
|
4
4
|
import { CommitField } from "./propertyPanelPrimitives";
|
|
5
|
+
import { FlatSliderReadout } from "./propertyPanelFlatSliderReadout";
|
|
5
6
|
import {
|
|
6
7
|
VALUE_TIER_LABEL_CLASS,
|
|
7
8
|
VALUE_TIER_VALUE_CLASS,
|
|
@@ -278,6 +279,7 @@ export function FlatSlider({
|
|
|
278
279
|
centerTick,
|
|
279
280
|
onReset,
|
|
280
281
|
onCommit,
|
|
282
|
+
onCommitText,
|
|
281
283
|
}: {
|
|
282
284
|
label: string;
|
|
283
285
|
value: number;
|
|
@@ -290,6 +292,8 @@ export function FlatSlider({
|
|
|
290
292
|
centerTick?: boolean;
|
|
291
293
|
onReset?: () => void;
|
|
292
294
|
onCommit: (nextValue: number) => void;
|
|
295
|
+
/** Typed readout: return false to refuse the text, keep the field open, and mark it invalid. */
|
|
296
|
+
onCommitText?: (text: string) => boolean | void;
|
|
293
297
|
}) {
|
|
294
298
|
const track = useTrackDesignInput();
|
|
295
299
|
// `draft` gives the knob instant, drag-local visual feedback. `onCommit` is
|
|
@@ -538,14 +542,14 @@ export function FlatSlider({
|
|
|
538
542
|
style={{ left: `${clampedPct}%` }}
|
|
539
543
|
/>
|
|
540
544
|
</div>
|
|
541
|
-
<
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
{
|
|
548
|
-
|
|
545
|
+
<FlatSliderReadout
|
|
546
|
+
label={label}
|
|
547
|
+
displayValue={displayValue}
|
|
548
|
+
tier={tier}
|
|
549
|
+
disabled={disabled}
|
|
550
|
+
onCommitText={onCommitText}
|
|
551
|
+
onCommitted={() => track("input", label)}
|
|
552
|
+
/>
|
|
549
553
|
{(centerTick || onReset) && (
|
|
550
554
|
<span data-flat-slider-reset-slot="true" className="w-3.5 shrink-0">
|
|
551
555
|
{tier === "explicitCustom" && onReset && (
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** Click-to-type readout. Enter/blur commits; a refused value stays open and turns red. */
|
|
4
|
+
export function FlatSliderReadout({
|
|
5
|
+
label,
|
|
6
|
+
displayValue,
|
|
7
|
+
tier,
|
|
8
|
+
disabled,
|
|
9
|
+
onCommitText,
|
|
10
|
+
onCommitted,
|
|
11
|
+
}: {
|
|
12
|
+
label: string;
|
|
13
|
+
displayValue: string;
|
|
14
|
+
tier: "default" | "explicitCustom";
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Receives the raw text; return false to refuse it and keep the field open. */
|
|
17
|
+
onCommitText?: (text: string) => boolean | void;
|
|
18
|
+
/** Fires after an accepted commit, for telemetry. */
|
|
19
|
+
onCommitted?: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
const [editing, setEditing] = useState(false);
|
|
22
|
+
const [text, setText] = useState("");
|
|
23
|
+
const [invalid, setInvalid] = useState(false);
|
|
24
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
25
|
+
const editable = Boolean(onCommitText) && !disabled;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!editing) return;
|
|
29
|
+
inputRef.current?.focus();
|
|
30
|
+
inputRef.current?.select();
|
|
31
|
+
}, [editing]);
|
|
32
|
+
|
|
33
|
+
const begin = () => {
|
|
34
|
+
if (!editable) return;
|
|
35
|
+
setText(displayValue);
|
|
36
|
+
setInvalid(false);
|
|
37
|
+
setEditing(true);
|
|
38
|
+
};
|
|
39
|
+
// Enter and blur both commit, so a value typed and then clicked away from is
|
|
40
|
+
// not silently dropped. A refused value stays open on Enter (the author is
|
|
41
|
+
// still there to fix it) and is discarded on blur (they have moved on).
|
|
42
|
+
const commit = (keepOpenIfRefused: boolean) => {
|
|
43
|
+
const accepted = onCommitText?.(text.trim());
|
|
44
|
+
if (accepted === false && keepOpenIfRefused) {
|
|
45
|
+
setInvalid(true);
|
|
46
|
+
inputRef.current?.select();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setEditing(false);
|
|
50
|
+
if (accepted !== false) onCommitted?.();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (editing) {
|
|
54
|
+
return (
|
|
55
|
+
<input
|
|
56
|
+
ref={inputRef}
|
|
57
|
+
data-flat-slider-input="true"
|
|
58
|
+
aria-label={`${label} value`}
|
|
59
|
+
aria-invalid={invalid || undefined}
|
|
60
|
+
value={text}
|
|
61
|
+
spellCheck={false}
|
|
62
|
+
onChange={(e) => {
|
|
63
|
+
setText(e.target.value);
|
|
64
|
+
setInvalid(false);
|
|
65
|
+
}}
|
|
66
|
+
onKeyDown={(e) => {
|
|
67
|
+
if (e.key === "Enter") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
commit(true);
|
|
70
|
+
} else if (e.key === "Escape") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
setEditing(false);
|
|
73
|
+
}
|
|
74
|
+
// Keep the timeline's own shortcuts from firing on every keystroke.
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
}}
|
|
77
|
+
onBlur={() => commit(false)}
|
|
78
|
+
className={`w-11 shrink-0 rounded-[3px] border bg-panel-surface px-1 text-right font-mono text-[10px] text-panel-text-0 outline-none ${
|
|
79
|
+
invalid ? "border-red-400" : "border-panel-accent"
|
|
80
|
+
}`}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<span
|
|
87
|
+
data-flat-slider-value="true"
|
|
88
|
+
role={editable ? "button" : undefined}
|
|
89
|
+
tabIndex={editable ? 0 : undefined}
|
|
90
|
+
title={editable ? "Click to type a value" : undefined}
|
|
91
|
+
onClick={begin}
|
|
92
|
+
onKeyDown={(e) => {
|
|
93
|
+
if (!editable) return;
|
|
94
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
begin();
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
className={`w-11 shrink-0 text-right font-mono text-[10px] ${
|
|
100
|
+
tier === "explicitCustom" ? "text-panel-text-0" : "text-panel-text-3"
|
|
101
|
+
} ${editable ? "cursor-text rounded-[3px] hover:bg-panel-hover hover:text-panel-text-0" : ""}`}
|
|
102
|
+
>
|
|
103
|
+
{displayValue}
|
|
104
|
+
</span>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mediaMetadataUrl } from "../../utils/studioHelpers";
|
|
1
2
|
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
3
4
|
import {
|
|
@@ -288,12 +289,7 @@ export function useColorGradingController({
|
|
|
288
289
|
return;
|
|
289
290
|
}
|
|
290
291
|
const controller = new AbortController();
|
|
291
|
-
fetch(
|
|
292
|
-
`/api/projects/${encodeURIComponent(projectId)}/media/metadata?path=${encodeURIComponent(
|
|
293
|
-
selectedAssetPath,
|
|
294
|
-
)}`,
|
|
295
|
-
{ signal: controller.signal },
|
|
296
|
-
)
|
|
292
|
+
fetch(mediaMetadataUrl(projectId, selectedAssetPath), { signal: controller.signal })
|
|
297
293
|
.then(async (response) => {
|
|
298
294
|
if (!response.ok) return { ok: false as const };
|
|
299
295
|
const data: MediaMetadataResponse | null = await response.json();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function AudioMetersIcon({ size = 16 }: { size?: number | string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
width={size}
|
|
5
|
+
height={size}
|
|
6
|
+
viewBox="0 0 16 16"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth={1.5}
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
>
|
|
13
|
+
<path d="M2.5 13h2m-2-3h2M7 13h2m-2-3h2m-2-3h2m-2-3h2M11.5 13h2m-2-3h2m-2-3h2" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function InspectorIcon({ size = 16 }: { size?: number | string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
width={size}
|
|
5
|
+
height={size}
|
|
6
|
+
viewBox="0 0 16 16"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth={1.5}
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
>
|
|
14
|
+
<rect x="1.5" y="2" width="13" height="12" rx="1.5" />
|
|
15
|
+
<path d="M4 6h8M6 4.5v3M4 10h8m-2-1.5v3" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function ReverseGradientIcon({ size = 16 }: { size?: number | string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
width={size}
|
|
5
|
+
height={size}
|
|
6
|
+
viewBox="0 0 16 16"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth={1.5}
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
>
|
|
14
|
+
<path d="M2 3h12m-2-2 2 2-2 2M14 8H2m2-2L2 8l2 2" />
|
|
15
|
+
<g fill="currentColor" stroke="none">
|
|
16
|
+
<path d="M2 12h3v2H2z" opacity="0.25" />
|
|
17
|
+
<path d="M5 12h3v2H5z" opacity="0.5" />
|
|
18
|
+
<path d="M8 12h3v2H8z" opacity="0.75" />
|
|
19
|
+
<path d="M11 12h3v2h-3z" />
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function RippleEditIcon({ size = 16 }: { size?: number | string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
width={size}
|
|
5
|
+
height={size}
|
|
6
|
+
viewBox="0 0 16 16"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth={1.5}
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
>
|
|
14
|
+
<rect x="1.5" y="8" width="4" height="5" rx="0.5" />
|
|
15
|
+
<rect x="10.5" y="8" width="4" height="5" rx="0.5" />
|
|
16
|
+
<path d="M13 4H6m2-2L6 4l2 2" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|