@dumiflex/wp-kit 0.3.0 → 0.4.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/CHANGELOG.md +77 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +119 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +292 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +125 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +173 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +175 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +82 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/floating.ts
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a floating panel (popover, menu, tooltip) goes next to its anchor:
|
|
3
|
-
* on the preferred side when it fits, on the other side when it doesn't,
|
|
4
|
-
* and always kept inside the viewport. Pure maths, no DOM, so it is the
|
|
5
|
-
* same on the canvas and in the manager. `Popover`, `ContextMenu` and the
|
|
6
|
-
* tooltip directive all place themselves through it.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type Side = "top" | "bottom" | "left" | "right";
|
|
10
|
-
export type Align = "start" | "center" | "end";
|
|
11
|
-
export type Placement = Side | `${Side}-${Exclude<Align, "center">}`;
|
|
12
|
-
|
|
13
|
-
export interface Rect {
|
|
14
|
-
left: number;
|
|
15
|
-
top: number;
|
|
16
|
-
width: number;
|
|
17
|
-
height: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface Size {
|
|
21
|
-
width: number;
|
|
22
|
-
height: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface PlaceOptions {
|
|
26
|
-
placement?: Placement;
|
|
27
|
-
/** Space between the anchor and the panel. */
|
|
28
|
-
gap?: number;
|
|
29
|
-
/** Space the panel keeps from the viewport edge. */
|
|
30
|
-
margin?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface Placed {
|
|
34
|
-
x: number;
|
|
35
|
-
y: number;
|
|
36
|
-
/** The side it ended up on, after any flip. */
|
|
37
|
-
side: Side;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const OPPOSITE: Record<Side, Side> = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
41
|
-
|
|
42
|
-
export function splitPlacement(p: Placement): { side: Side; align: Align } {
|
|
43
|
-
const [side, align] = p.split("-") as [Side, Exclude<Align, "center"> | undefined];
|
|
44
|
-
return { side, align: align ?? "center" };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function room(side: Side, a: Rect, view: Size, gap: number, margin: number): number {
|
|
48
|
-
switch (side) {
|
|
49
|
-
case "top": return a.top - gap - margin;
|
|
50
|
-
case "bottom": return view.height - (a.top + a.height) - gap - margin;
|
|
51
|
-
case "left": return a.left - gap - margin;
|
|
52
|
-
case "right": return view.width - (a.left + a.width) - gap - margin;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function clamp(v: number, min: number, max: number): number {
|
|
57
|
-
return max < min ? min : Math.min(Math.max(v, min), max);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function placeFloating(anchor: Rect, panel: Size, view: Size, opts: PlaceOptions = {}): Placed {
|
|
61
|
-
const gap = opts.gap ?? 6;
|
|
62
|
-
const margin = opts.margin ?? 8;
|
|
63
|
-
const { side: want, align } = splitPlacement(opts.placement ?? "bottom-start");
|
|
64
|
-
const vertical = want === "top" || want === "bottom";
|
|
65
|
-
const need = vertical ? panel.height : panel.width;
|
|
66
|
-
|
|
67
|
-
// Flip only when the other side has more room; a panel too big for both
|
|
68
|
-
// stays on the side with more space and gets clamped.
|
|
69
|
-
let side = want;
|
|
70
|
-
if (room(want, anchor, view, gap, margin) < need) {
|
|
71
|
-
const other = OPPOSITE[want];
|
|
72
|
-
if (room(other, anchor, view, gap, margin) > room(want, anchor, view, gap, margin)) side = other;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let x: number;
|
|
76
|
-
let y: number;
|
|
77
|
-
if (vertical) {
|
|
78
|
-
y = side === "bottom" ? anchor.top + anchor.height + gap : anchor.top - gap - panel.height;
|
|
79
|
-
x = align === "start" ? anchor.left
|
|
80
|
-
: align === "end" ? anchor.left + anchor.width - panel.width
|
|
81
|
-
: anchor.left + (anchor.width - panel.width) / 2;
|
|
82
|
-
} else {
|
|
83
|
-
x = side === "right" ? anchor.left + anchor.width + gap : anchor.left - gap - panel.width;
|
|
84
|
-
y = align === "start" ? anchor.top
|
|
85
|
-
: align === "end" ? anchor.top + anchor.height - panel.height
|
|
86
|
-
: anchor.top + (anchor.height - panel.height) / 2;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
x: Math.round(clamp(x, margin, view.width - margin - panel.width)),
|
|
91
|
-
y: Math.round(clamp(y, margin, view.height - margin - panel.height)),
|
|
92
|
-
side,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The viewport, for callers that have a window. */
|
|
97
|
-
export function viewportSize(): Size {
|
|
98
|
-
return { width: window.innerWidth, height: window.innerHeight };
|
|
99
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Where a floating panel (popover, menu, tooltip) goes next to its anchor:
|
|
3
|
+
* on the preferred side when it fits, on the other side when it doesn't,
|
|
4
|
+
* and always kept inside the viewport. Pure maths, no DOM, so it is the
|
|
5
|
+
* same on the canvas and in the manager. `Popover`, `ContextMenu` and the
|
|
6
|
+
* tooltip directive all place themselves through it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Side = "top" | "bottom" | "left" | "right";
|
|
10
|
+
export type Align = "start" | "center" | "end";
|
|
11
|
+
export type Placement = Side | `${Side}-${Exclude<Align, "center">}`;
|
|
12
|
+
|
|
13
|
+
export interface Rect {
|
|
14
|
+
left: number;
|
|
15
|
+
top: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Size {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PlaceOptions {
|
|
26
|
+
placement?: Placement;
|
|
27
|
+
/** Space between the anchor and the panel. */
|
|
28
|
+
gap?: number;
|
|
29
|
+
/** Space the panel keeps from the viewport edge. */
|
|
30
|
+
margin?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Placed {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
/** The side it ended up on, after any flip. */
|
|
37
|
+
side: Side;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const OPPOSITE: Record<Side, Side> = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
41
|
+
|
|
42
|
+
export function splitPlacement(p: Placement): { side: Side; align: Align } {
|
|
43
|
+
const [side, align] = p.split("-") as [Side, Exclude<Align, "center"> | undefined];
|
|
44
|
+
return { side, align: align ?? "center" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function room(side: Side, a: Rect, view: Size, gap: number, margin: number): number {
|
|
48
|
+
switch (side) {
|
|
49
|
+
case "top": return a.top - gap - margin;
|
|
50
|
+
case "bottom": return view.height - (a.top + a.height) - gap - margin;
|
|
51
|
+
case "left": return a.left - gap - margin;
|
|
52
|
+
case "right": return view.width - (a.left + a.width) - gap - margin;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clamp(v: number, min: number, max: number): number {
|
|
57
|
+
return max < min ? min : Math.min(Math.max(v, min), max);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function placeFloating(anchor: Rect, panel: Size, view: Size, opts: PlaceOptions = {}): Placed {
|
|
61
|
+
const gap = opts.gap ?? 6;
|
|
62
|
+
const margin = opts.margin ?? 8;
|
|
63
|
+
const { side: want, align } = splitPlacement(opts.placement ?? "bottom-start");
|
|
64
|
+
const vertical = want === "top" || want === "bottom";
|
|
65
|
+
const need = vertical ? panel.height : panel.width;
|
|
66
|
+
|
|
67
|
+
// Flip only when the other side has more room; a panel too big for both
|
|
68
|
+
// stays on the side with more space and gets clamped.
|
|
69
|
+
let side = want;
|
|
70
|
+
if (room(want, anchor, view, gap, margin) < need) {
|
|
71
|
+
const other = OPPOSITE[want];
|
|
72
|
+
if (room(other, anchor, view, gap, margin) > room(want, anchor, view, gap, margin)) side = other;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let x: number;
|
|
76
|
+
let y: number;
|
|
77
|
+
if (vertical) {
|
|
78
|
+
y = side === "bottom" ? anchor.top + anchor.height + gap : anchor.top - gap - panel.height;
|
|
79
|
+
x = align === "start" ? anchor.left
|
|
80
|
+
: align === "end" ? anchor.left + anchor.width - panel.width
|
|
81
|
+
: anchor.left + (anchor.width - panel.width) / 2;
|
|
82
|
+
} else {
|
|
83
|
+
x = side === "right" ? anchor.left + anchor.width + gap : anchor.left - gap - panel.width;
|
|
84
|
+
y = align === "start" ? anchor.top
|
|
85
|
+
: align === "end" ? anchor.top + anchor.height - panel.height
|
|
86
|
+
: anchor.top + (anchor.height - panel.height) / 2;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
x: Math.round(clamp(x, margin, view.width - margin - panel.width)),
|
|
91
|
+
y: Math.round(clamp(y, margin, view.height - margin - panel.height)),
|
|
92
|
+
side,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The viewport, for callers that have a window. */
|
|
97
|
+
export function viewportSize(): Size {
|
|
98
|
+
return { width: window.innerWidth, height: window.innerHeight };
|
|
99
|
+
}
|
package/src/hsv.ts
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HSV / RGB / hex color conversion utilities.
|
|
3
|
-
*
|
|
4
|
-
* `h` in `[0, 360)`, `s` and `v` in `[0, 1]`. Hex strings are `#rrggbb`
|
|
5
|
-
* lowercase. Functions are tolerant of out-of-range input — they clamp
|
|
6
|
-
* rather than throw, which keeps the picker UI robust under fast drags.
|
|
7
|
-
*
|
|
8
|
-
* Lives in shared/ so both the manager-side ColorPicker / TweaksPanel
|
|
9
|
-
* AND the canvas-side bundle modal can drive the same HsvPicker
|
|
10
|
-
* without a cross-package import.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export interface Hsv {
|
|
14
|
-
h: number;
|
|
15
|
-
s: number;
|
|
16
|
-
v: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface Rgb {
|
|
20
|
-
r: number;
|
|
21
|
-
g: number;
|
|
22
|
-
b: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const HEX_PATTERN = /^#?([0-9a-fA-F]{6})$/;
|
|
26
|
-
|
|
27
|
-
function clamp(n: number, lo: number, hi: number): number {
|
|
28
|
-
if (Number.isNaN(n)) return lo;
|
|
29
|
-
return Math.min(hi, Math.max(lo, n));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function hsvToRgb(hsv: Hsv): Rgb {
|
|
33
|
-
const H = (((hsv.h % 360) + 360) % 360);
|
|
34
|
-
const S = clamp(hsv.s, 0, 1);
|
|
35
|
-
const V = clamp(hsv.v, 0, 1);
|
|
36
|
-
|
|
37
|
-
const C = V * S;
|
|
38
|
-
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
|
|
39
|
-
const M = V - C;
|
|
40
|
-
let r1 = 0;
|
|
41
|
-
let g1 = 0;
|
|
42
|
-
let b1 = 0;
|
|
43
|
-
if (H < 60) { r1 = C; g1 = X; b1 = 0; }
|
|
44
|
-
else if (H < 120) { r1 = X; g1 = C; b1 = 0; }
|
|
45
|
-
else if (H < 180) { r1 = 0; g1 = C; b1 = X; }
|
|
46
|
-
else if (H < 240) { r1 = 0; g1 = X; b1 = C; }
|
|
47
|
-
else if (H < 300) { r1 = X; g1 = 0; b1 = C; }
|
|
48
|
-
else { r1 = C; g1 = 0; b1 = X; }
|
|
49
|
-
return {
|
|
50
|
-
r: Math.round((r1 + M) * 255),
|
|
51
|
-
g: Math.round((g1 + M) * 255),
|
|
52
|
-
b: Math.round((b1 + M) * 255),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function rgbToHsv(rgb: Rgb): Hsv {
|
|
57
|
-
const R = clamp(rgb.r, 0, 255) / 255;
|
|
58
|
-
const G = clamp(rgb.g, 0, 255) / 255;
|
|
59
|
-
const B = clamp(rgb.b, 0, 255) / 255;
|
|
60
|
-
const max = Math.max(R, G, B);
|
|
61
|
-
const min = Math.min(R, G, B);
|
|
62
|
-
const delta = max - min;
|
|
63
|
-
|
|
64
|
-
let h = 0;
|
|
65
|
-
if (delta > 0) {
|
|
66
|
-
if (max === R) h = ((G - B) / delta) % 6;
|
|
67
|
-
else if (max === G) h = (B - R) / delta + 2;
|
|
68
|
-
else h = (R - G) / delta + 4;
|
|
69
|
-
h *= 60;
|
|
70
|
-
if (h < 0) h += 360;
|
|
71
|
-
}
|
|
72
|
-
const s = max === 0 ? 0 : delta / max;
|
|
73
|
-
const v = max;
|
|
74
|
-
return { h, s, v };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function rgbToHex(rgb: Rgb): string {
|
|
78
|
-
const part = (n: number): string => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
|
|
79
|
-
return `#${part(rgb.r)}${part(rgb.g)}${part(rgb.b)}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function hexToRgb(hex: string): Rgb | null {
|
|
83
|
-
const matches = hex.trim().match(HEX_PATTERN);
|
|
84
|
-
if (!matches) return null;
|
|
85
|
-
const v = matches[1];
|
|
86
|
-
return {
|
|
87
|
-
r: parseInt(v.slice(0, 2), 16),
|
|
88
|
-
g: parseInt(v.slice(2, 4), 16),
|
|
89
|
-
b: parseInt(v.slice(4, 6), 16),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function hexToHsv(hex: string): Hsv | null {
|
|
94
|
-
const rgb = hexToRgb(hex);
|
|
95
|
-
return rgb ? rgbToHsv(rgb) : null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function hsvToHex(hsv: Hsv): string {
|
|
99
|
-
return rgbToHex(hsvToRgb(hsv));
|
|
100
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* HSV / RGB / hex color conversion utilities.
|
|
3
|
+
*
|
|
4
|
+
* `h` in `[0, 360)`, `s` and `v` in `[0, 1]`. Hex strings are `#rrggbb`
|
|
5
|
+
* lowercase. Functions are tolerant of out-of-range input — they clamp
|
|
6
|
+
* rather than throw, which keeps the picker UI robust under fast drags.
|
|
7
|
+
*
|
|
8
|
+
* Lives in shared/ so both the manager-side ColorPicker / TweaksPanel
|
|
9
|
+
* AND the canvas-side bundle modal can drive the same HsvPicker
|
|
10
|
+
* without a cross-package import.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Hsv {
|
|
14
|
+
h: number;
|
|
15
|
+
s: number;
|
|
16
|
+
v: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Rgb {
|
|
20
|
+
r: number;
|
|
21
|
+
g: number;
|
|
22
|
+
b: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HEX_PATTERN = /^#?([0-9a-fA-F]{6})$/;
|
|
26
|
+
|
|
27
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
28
|
+
if (Number.isNaN(n)) return lo;
|
|
29
|
+
return Math.min(hi, Math.max(lo, n));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hsvToRgb(hsv: Hsv): Rgb {
|
|
33
|
+
const H = (((hsv.h % 360) + 360) % 360);
|
|
34
|
+
const S = clamp(hsv.s, 0, 1);
|
|
35
|
+
const V = clamp(hsv.v, 0, 1);
|
|
36
|
+
|
|
37
|
+
const C = V * S;
|
|
38
|
+
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
|
|
39
|
+
const M = V - C;
|
|
40
|
+
let r1 = 0;
|
|
41
|
+
let g1 = 0;
|
|
42
|
+
let b1 = 0;
|
|
43
|
+
if (H < 60) { r1 = C; g1 = X; b1 = 0; }
|
|
44
|
+
else if (H < 120) { r1 = X; g1 = C; b1 = 0; }
|
|
45
|
+
else if (H < 180) { r1 = 0; g1 = C; b1 = X; }
|
|
46
|
+
else if (H < 240) { r1 = 0; g1 = X; b1 = C; }
|
|
47
|
+
else if (H < 300) { r1 = X; g1 = 0; b1 = C; }
|
|
48
|
+
else { r1 = C; g1 = 0; b1 = X; }
|
|
49
|
+
return {
|
|
50
|
+
r: Math.round((r1 + M) * 255),
|
|
51
|
+
g: Math.round((g1 + M) * 255),
|
|
52
|
+
b: Math.round((b1 + M) * 255),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function rgbToHsv(rgb: Rgb): Hsv {
|
|
57
|
+
const R = clamp(rgb.r, 0, 255) / 255;
|
|
58
|
+
const G = clamp(rgb.g, 0, 255) / 255;
|
|
59
|
+
const B = clamp(rgb.b, 0, 255) / 255;
|
|
60
|
+
const max = Math.max(R, G, B);
|
|
61
|
+
const min = Math.min(R, G, B);
|
|
62
|
+
const delta = max - min;
|
|
63
|
+
|
|
64
|
+
let h = 0;
|
|
65
|
+
if (delta > 0) {
|
|
66
|
+
if (max === R) h = ((G - B) / delta) % 6;
|
|
67
|
+
else if (max === G) h = (B - R) / delta + 2;
|
|
68
|
+
else h = (R - G) / delta + 4;
|
|
69
|
+
h *= 60;
|
|
70
|
+
if (h < 0) h += 360;
|
|
71
|
+
}
|
|
72
|
+
const s = max === 0 ? 0 : delta / max;
|
|
73
|
+
const v = max;
|
|
74
|
+
return { h, s, v };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function rgbToHex(rgb: Rgb): string {
|
|
78
|
+
const part = (n: number): string => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
|
|
79
|
+
return `#${part(rgb.r)}${part(rgb.g)}${part(rgb.b)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function hexToRgb(hex: string): Rgb | null {
|
|
83
|
+
const matches = hex.trim().match(HEX_PATTERN);
|
|
84
|
+
if (!matches) return null;
|
|
85
|
+
const v = matches[1];
|
|
86
|
+
return {
|
|
87
|
+
r: parseInt(v.slice(0, 2), 16),
|
|
88
|
+
g: parseInt(v.slice(2, 4), 16),
|
|
89
|
+
b: parseInt(v.slice(4, 6), 16),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hexToHsv(hex: string): Hsv | null {
|
|
94
|
+
const rgb = hexToRgb(hex);
|
|
95
|
+
return rgb ? rgbToHsv(rgb) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function hsvToHex(hsv: Hsv): string {
|
|
99
|
+
return rgbToHex(hsvToRgb(hsv));
|
|
100
|
+
}
|
package/src/icon-catalog.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Curated PrimeIcons glyphs offered by `IconPicker.vue`.
|
|
3
|
-
*
|
|
4
|
-
* PrimeIcons ships ~314 glyphs; this is a subset. The full set would need its
|
|
5
|
-
* own search UI, and most of it (chevrons, spinners, media transport, brand
|
|
6
|
-
* logos) means nothing as a library label. The groups below follow the kinds
|
|
7
|
-
* of things people actually categorise prompt modules by.
|
|
8
|
-
*
|
|
9
|
-
* Names are PrimeIcons ids MINUS the `pi-` prefix (`"user"`, not `"pi-user"`)
|
|
10
|
-
* — that is what the engine's `icon` column holds and what `Icon.vue` expects.
|
|
11
|
-
*
|
|
12
|
-
* In a plain module rather than inside the SFC so `icon-catalog.test.ts` can
|
|
13
|
-
* check every name against the installed `primeicons.css` without mounting a
|
|
14
|
-
* component. That test is the point: a name with no matching glyph renders as
|
|
15
|
-
* an EMPTY CELL, which looks like a layout bug rather than a typo, and `brush`
|
|
16
|
-
* shipped that way because nothing verified the list.
|
|
17
|
-
*/
|
|
18
|
-
export interface IconGroup {
|
|
19
|
-
label: string;
|
|
20
|
-
icons: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const ICON_GROUPS: IconGroup[] = [
|
|
24
|
-
{
|
|
25
|
-
label: "Subject",
|
|
26
|
-
icons: ["user", "users", "heart", "star", "eye", "face-smile", "crown", "id-card"],
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
// `sliders-h` stands in for the art-supply glyph PrimeIcons doesn't have —
|
|
30
|
-
// there is no brush/paint icon in the set, and `palette` already carries
|
|
31
|
-
// "art style". Sliders read as "tune the look".
|
|
32
|
-
label: "Style & look",
|
|
33
|
-
icons: ["palette", "pencil", "sliders-h", "camera", "image", "sparkles", "sun", "moon"],
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
label: "Scene",
|
|
37
|
-
icons: ["home", "building", "map", "map-marker", "globe", "compass", "car", "cloud"],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
label: "Structure",
|
|
41
|
-
icons: ["box", "folder", "book", "tag", "tags", "list", "sitemap", "objects-column"],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
label: "Signals",
|
|
45
|
-
icons: ["bolt", "flag", "bell", "shield", "lock", "wrench", "filter", "info-circle"],
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/** Every offered glyph, flattened — for tests and any future search box. */
|
|
50
|
-
export function allCuratedIcons(): string[] {
|
|
51
|
-
return ICON_GROUPS.flatMap((g) => g.icons);
|
|
52
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Curated PrimeIcons glyphs offered by `IconPicker.vue`.
|
|
3
|
+
*
|
|
4
|
+
* PrimeIcons ships ~314 glyphs; this is a subset. The full set would need its
|
|
5
|
+
* own search UI, and most of it (chevrons, spinners, media transport, brand
|
|
6
|
+
* logos) means nothing as a library label. The groups below follow the kinds
|
|
7
|
+
* of things people actually categorise prompt modules by.
|
|
8
|
+
*
|
|
9
|
+
* Names are PrimeIcons ids MINUS the `pi-` prefix (`"user"`, not `"pi-user"`)
|
|
10
|
+
* — that is what the engine's `icon` column holds and what `Icon.vue` expects.
|
|
11
|
+
*
|
|
12
|
+
* In a plain module rather than inside the SFC so `icon-catalog.test.ts` can
|
|
13
|
+
* check every name against the installed `primeicons.css` without mounting a
|
|
14
|
+
* component. That test is the point: a name with no matching glyph renders as
|
|
15
|
+
* an EMPTY CELL, which looks like a layout bug rather than a typo, and `brush`
|
|
16
|
+
* shipped that way because nothing verified the list.
|
|
17
|
+
*/
|
|
18
|
+
export interface IconGroup {
|
|
19
|
+
label: string;
|
|
20
|
+
icons: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ICON_GROUPS: IconGroup[] = [
|
|
24
|
+
{
|
|
25
|
+
label: "Subject",
|
|
26
|
+
icons: ["user", "users", "heart", "star", "eye", "face-smile", "crown", "id-card"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
// `sliders-h` stands in for the art-supply glyph PrimeIcons doesn't have —
|
|
30
|
+
// there is no brush/paint icon in the set, and `palette` already carries
|
|
31
|
+
// "art style". Sliders read as "tune the look".
|
|
32
|
+
label: "Style & look",
|
|
33
|
+
icons: ["palette", "pencil", "sliders-h", "camera", "image", "sparkles", "sun", "moon"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "Scene",
|
|
37
|
+
icons: ["home", "building", "map", "map-marker", "globe", "compass", "car", "cloud"],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "Structure",
|
|
41
|
+
icons: ["box", "folder", "book", "tag", "tags", "list", "sitemap", "objects-column"],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "Signals",
|
|
45
|
+
icons: ["bolt", "flag", "bell", "shield", "lock", "wrench", "filter", "info-circle"],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Every offered glyph, flattened — for tests and any future search box. */
|
|
50
|
+
export function allCuratedIcons(): string[] {
|
|
51
|
+
return ICON_GROUPS.flatMap((g) => g.icons);
|
|
52
|
+
}
|