@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. 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
+ }
@@ -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
+ }