@dumiflex/wp-kit 0.3.0 → 0.5.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 +92 -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 +146 -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 +293 -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 +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -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 +98 -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/highlight.ts +199 -0
- 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/loading-mark-motion.ts +249 -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/highlight.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax colours for CodeBlock: a small lexer per language, no dependency.
|
|
3
|
+
* It splits code into tokens of a few kinds; CodeBlock draws each kind in a
|
|
4
|
+
* shared colour token, so a block wears the colours of the side it is on.
|
|
5
|
+
*
|
|
6
|
+
* highlight('{"a": 1}', "json") → [{ kind: "punct", text: "{" }, { kind: "key", text: '"a"' }, …]
|
|
7
|
+
*
|
|
8
|
+
* Languages: json, ts (also js), html (also vue, xml), css, sh (also bash,
|
|
9
|
+
* shell, Modelfile), prompt (the Wildcard Pipeline template grammar: `$var`,
|
|
10
|
+
* `@{ref}`, `{a|b}`, `{2$$a|b}`, `$$` / `@@` escapes, `(word:1.2)` weights). Anything
|
|
11
|
+
* else comes back as one plain token. Every character of the input is in
|
|
12
|
+
* exactly one token, in order, so joining the tokens gives the input back.
|
|
13
|
+
*/
|
|
14
|
+
import { tokenizeRich } from "./rich-tokenize";
|
|
15
|
+
|
|
16
|
+
export type HighlightKind =
|
|
17
|
+
| "plain" | "keyword" | "string" | "number" | "comment" | "punct"
|
|
18
|
+
| "key" | "tag" | "attr" | "fn" | "var" | "ref" | "brace" | "escape";
|
|
19
|
+
|
|
20
|
+
export interface HighlightToken {
|
|
21
|
+
kind: HighlightKind;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type HighlightLanguage = "json" | "ts" | "html" | "css" | "sh" | "prompt";
|
|
26
|
+
|
|
27
|
+
const ALIASES: Record<string, HighlightLanguage> = {
|
|
28
|
+
json: "json",
|
|
29
|
+
ts: "ts", typescript: "ts", js: "ts", javascript: "ts", tsx: "ts", jsx: "ts",
|
|
30
|
+
html: "html", vue: "html", xml: "html", svg: "html",
|
|
31
|
+
css: "css",
|
|
32
|
+
sh: "sh", bash: "sh", shell: "sh", zsh: "sh", console: "sh", modelfile: "sh", dockerfile: "sh",
|
|
33
|
+
prompt: "prompt", template: "prompt", wildcard: "prompt",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The grammar for a language name ("JSON", "Vue", "bash" …), or null. */
|
|
37
|
+
export function highlightLanguage(name: string | null | undefined): HighlightLanguage | null {
|
|
38
|
+
if (!name) return null;
|
|
39
|
+
return ALIASES[name.trim().toLowerCase()] ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Rule = [HighlightKind, RegExp];
|
|
43
|
+
|
|
44
|
+
const TS_KEYWORDS = /^(?:import|from|export|default|const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|class|extends|interface|type|enum|as|in|of|typeof|instanceof|async|await|try|catch|finally|throw|void|this|null|undefined|true|false|readonly|public|private|protected|static|satisfies|keyof)$/;
|
|
45
|
+
|
|
46
|
+
const RULES: Record<Exclude<HighlightLanguage, "prompt" | "html">, Rule[]> = {
|
|
47
|
+
json: [
|
|
48
|
+
["key", /"(?:[^"\\\n]|\\.)*"(?=\s*:)/y],
|
|
49
|
+
["string", /"(?:[^"\\\n]|\\.)*"/y],
|
|
50
|
+
["number", /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y],
|
|
51
|
+
["keyword", /\b(?:true|false|null)\b/y],
|
|
52
|
+
["punct", /[{}[\],:]/y],
|
|
53
|
+
],
|
|
54
|
+
ts: [
|
|
55
|
+
["comment", /\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/y],
|
|
56
|
+
["string", /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`/y],
|
|
57
|
+
["number", /\b\d+(?:\.\d+)?\b/y],
|
|
58
|
+
["fn", /[A-Za-z_$][\w$]*(?=\s*\()/y],
|
|
59
|
+
["plain", /[A-Za-z_$][\w$]*/y],
|
|
60
|
+
["punct", /[{}()[\];,.<>=!&|?:+\-*/%]+/y],
|
|
61
|
+
],
|
|
62
|
+
css: [
|
|
63
|
+
["comment", /\/\*[\s\S]*?(?:\*\/|$)/y],
|
|
64
|
+
["string", /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/y],
|
|
65
|
+
["key", /[\w-]+(?=\s*:[^:{]*[;}\n])/y],
|
|
66
|
+
["var", /--[\w-]+/y],
|
|
67
|
+
["number", /#[\da-fA-F]{3,8}\b|-?\d*\.?\d+(?:px|em|rem|%|vh|vw|s|ms|deg|fr)?\b/y],
|
|
68
|
+
["fn", /[\w-]+(?=\()/y],
|
|
69
|
+
["tag", /[.#]?[\w-]+(?::{1,2}[\w-]+)*(?=[^{};]*\{)/y],
|
|
70
|
+
["punct", /[{}();:,>+~*]/y],
|
|
71
|
+
],
|
|
72
|
+
sh: [
|
|
73
|
+
["comment", /#[^\n]*/y],
|
|
74
|
+
["string", /"(?:[^"\\\n]|\\.)*"|'[^'\n]*'/y],
|
|
75
|
+
["var", /\$\{?[\w]+\}?/y],
|
|
76
|
+
["attr", /(?<=\s)--?[\w-]+/y],
|
|
77
|
+
["fn", /(?<=^|\n|[|;&]\s*)[A-Za-z_][\w.-]*/y],
|
|
78
|
+
["punct", /[|&;<>()]+/y],
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function lex(code: string, rules: Rule[]): HighlightToken[] {
|
|
83
|
+
const out: HighlightToken[] = [];
|
|
84
|
+
let plain = "";
|
|
85
|
+
let i = 0;
|
|
86
|
+
const flush = (): void => { if (plain) { out.push({ kind: "plain", text: plain }); plain = ""; } };
|
|
87
|
+
outer: while (i < code.length) {
|
|
88
|
+
for (const [kind, re] of rules) {
|
|
89
|
+
re.lastIndex = i;
|
|
90
|
+
const m = re.exec(code);
|
|
91
|
+
if (m && m[0].length > 0) {
|
|
92
|
+
let k = kind;
|
|
93
|
+
if (rules === RULES.ts && kind === "plain" && TS_KEYWORDS.test(m[0])) k = "keyword";
|
|
94
|
+
if (k === "plain") plain += m[0];
|
|
95
|
+
else { flush(); out.push({ kind: k, text: m[0] }); }
|
|
96
|
+
i += m[0].length;
|
|
97
|
+
continue outer;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
plain += code[i];
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
flush();
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** HTML and Vue templates: tags, attributes, their values, `{{ }}` as TS. */
|
|
108
|
+
function lexHtml(code: string): HighlightToken[] {
|
|
109
|
+
const out: HighlightToken[] = [];
|
|
110
|
+
const re = /<!--[\s\S]*?(?:-->|$)|<\/?[A-Za-z][\w.:-]*|\/?>|\{\{[\s\S]*?(?:\}\}|$)/g;
|
|
111
|
+
let at = 0;
|
|
112
|
+
let inTag = false;
|
|
113
|
+
const text = (s: string): void => {
|
|
114
|
+
if (!s) return;
|
|
115
|
+
if (!inTag) { out.push({ kind: "plain", text: s }); return; }
|
|
116
|
+
// Attributes inside a tag: name, =, "value".
|
|
117
|
+
const attr = /("[^"]*"|'[^']*')|([:@#]?[\w.:@#-]+)|(=)|(\s+)|([^\s"'=]+)/g;
|
|
118
|
+
for (const m of s.matchAll(attr)) {
|
|
119
|
+
if (m[1]) out.push({ kind: "string", text: m[1] });
|
|
120
|
+
else if (m[2]) out.push({ kind: "attr", text: m[2] });
|
|
121
|
+
else if (m[3]) out.push({ kind: "punct", text: m[3] });
|
|
122
|
+
else out.push({ kind: "plain", text: m[0] });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
for (const m of code.matchAll(re)) {
|
|
126
|
+
const idx = m.index ?? 0;
|
|
127
|
+
text(code.slice(at, idx));
|
|
128
|
+
const t = m[0];
|
|
129
|
+
if (t.startsWith("<!--")) out.push({ kind: "comment", text: t });
|
|
130
|
+
else if (t.startsWith("{{") && !inTag) {
|
|
131
|
+
out.push({ kind: "punct", text: "{{" });
|
|
132
|
+
const inner = t.slice(2, t.endsWith("}}") ? -2 : undefined);
|
|
133
|
+
out.push(...lex(inner, RULES.ts));
|
|
134
|
+
if (t.endsWith("}}")) out.push({ kind: "punct", text: "}}" });
|
|
135
|
+
} else if (t.startsWith("<")) {
|
|
136
|
+
out.push({ kind: "punct", text: t.startsWith("</") ? "</" : "<" });
|
|
137
|
+
out.push({ kind: "tag", text: t.replace(/^<\/?/, "") });
|
|
138
|
+
inTag = true;
|
|
139
|
+
} else if (t.endsWith(">")) {
|
|
140
|
+
out.push({ kind: "punct", text: t });
|
|
141
|
+
inTag = false;
|
|
142
|
+
} else {
|
|
143
|
+
text(t);
|
|
144
|
+
}
|
|
145
|
+
at = idx + t.length;
|
|
146
|
+
}
|
|
147
|
+
text(code.slice(at));
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The template grammar, through the kit's own tokenizer; `(word:1.2)` weights too. */
|
|
152
|
+
function lexPrompt(code: string): HighlightToken[] {
|
|
153
|
+
const out: HighlightToken[] = [];
|
|
154
|
+
const plain = (text: string): void => {
|
|
155
|
+
// Plain text: pick out `:1.2)` weights.
|
|
156
|
+
let at = 0;
|
|
157
|
+
for (const m of text.matchAll(/:(-?\d+(?:\.\d+)?)(?=\))/g)) {
|
|
158
|
+
const idx = (m.index ?? 0) + 1;
|
|
159
|
+
if (idx > at) out.push({ kind: "plain", text: text.slice(at, idx) });
|
|
160
|
+
out.push({ kind: "number", text: m[1] ?? "" });
|
|
161
|
+
at = idx + (m[1]?.length ?? 0);
|
|
162
|
+
}
|
|
163
|
+
if (at < text.length) out.push({ kind: "plain", text: text.slice(at) });
|
|
164
|
+
};
|
|
165
|
+
let at = 0;
|
|
166
|
+
for (const t of tokenizeRich(code)) {
|
|
167
|
+
// The tokenizer may leave gaps (it reports what it recognises); keep them as text.
|
|
168
|
+
if (t.start > at) plain(code.slice(at, t.start));
|
|
169
|
+
const text = code.slice(t.start, t.end);
|
|
170
|
+
if (t.kind === "var") out.push({ kind: "var", text });
|
|
171
|
+
else if (t.kind === "ref") out.push({ kind: "ref", text });
|
|
172
|
+
else if (t.kind === "dp-brace" || t.kind === "dp-multi") out.push({ kind: "brace", text });
|
|
173
|
+
else if (t.kind === "escape") out.push({ kind: "escape", text });
|
|
174
|
+
else plain(text);
|
|
175
|
+
at = Math.max(at, t.end);
|
|
176
|
+
}
|
|
177
|
+
if (at < code.length) plain(code.slice(at));
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function highlight(code: string, language: HighlightLanguage | null): HighlightToken[] {
|
|
182
|
+
if (!language || !code) return [{ kind: "plain", text: code }];
|
|
183
|
+
if (language === "html") return lexHtml(code);
|
|
184
|
+
if (language === "prompt") return lexPrompt(code);
|
|
185
|
+
return lex(code, RULES[language]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Tokens cut at line breaks, for numbered lines. */
|
|
189
|
+
export function highlightLines(tokens: HighlightToken[]): HighlightToken[][] {
|
|
190
|
+
const lines: HighlightToken[][] = [[]];
|
|
191
|
+
for (const t of tokens) {
|
|
192
|
+
const parts = t.text.split("\n");
|
|
193
|
+
parts.forEach((p, i) => {
|
|
194
|
+
if (i > 0) lines.push([]);
|
|
195
|
+
if (p) lines[lines.length - 1]?.push({ kind: t.kind, text: p });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
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
|
+
}
|