@leavepulse/ui 0.29.4 → 0.30.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/dist/assets/LpChart-CzgCGaAA.css +8 -0
- package/dist/chunks/LpActionBar-CxmBZpGu.js +173 -0
- package/dist/chunks/{LpAppShell-BJk3b-u4.js → LpAppShell-DnocEJAu.js} +1 -1
- package/dist/chunks/{LpCodeBlock-rS_6mO3D.js → LpCodeBlock-BWX1KZwk.js} +7 -13
- package/dist/chunks/LpColorPicker-CFC6iaNK.js +491 -0
- package/dist/chunks/{LpDisclosure-BnwP4Q-b.js → LpDisclosure-C1_YkMrd.js} +8 -2
- package/dist/chunks/{LpLogViewer-CVaGfTF_.js → LpLogViewer-TSRyt3t9.js} +3 -6
- package/dist/chunks/{LpSidebar-C_Mr-JPI.js → LpSidebar-CWSbczWi.js} +11 -2
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpActionBar.vue.d.ts +84 -0
- package/dist/components/LpActionBar.vue.js +4 -0
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpChart.vue.js +814 -2
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +124 -0
- package/dist/components/LpColorPicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +2 -2
- package/dist/components/LpSidebar.vue.d.ts +21 -0
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/actionBar.d.ts +19 -0
- package/dist/components/actionBar.js +31 -0
- package/dist/components/color.d.ts +65 -0
- package/dist/components/color.js +158 -0
- package/dist/composables/useClipboard.d.ts +46 -0
- package/dist/composables/useClipboard.js +28 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +154 -135
- package/package.json +1 -1
- package/src/component-names.ts +2 -0
- package/src/components/LpActionBar.vue +268 -0
- package/src/components/LpChart.vue +337 -63
- package/src/components/LpCodeBlock.vue +5 -10
- package/src/components/LpColorPicker.vue +784 -0
- package/src/components/LpDisclosure.vue +17 -2
- package/src/components/LpLogViewer.vue +5 -8
- package/src/components/LpSidebar.vue +42 -6
- package/src/components/actionBar.ts +73 -0
- package/src/components/color.ts +257 -0
- package/src/composables/useClipboard.ts +107 -0
- package/src/index.ts +21 -0
- package/dist/chunks/LpChart-Bmw7y34Z.js +0 -672
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ColorFormat } from './color';
|
|
2
|
+
import { ContextMenuItemDef } from './LpContextMenu.vue';
|
|
3
|
+
/**
|
|
4
|
+
* Every user-facing string the picker renders on its own — the menu items, the
|
|
5
|
+
* "Recent" heading, and the accessible names the area, rails and text field
|
|
6
|
+
* carry. Apps with i18n pass their translations in; the English defaults keep
|
|
7
|
+
* the component usable bare. Without this an app in another language would
|
|
8
|
+
* still announce "Saturation and brightness" to a screen reader.
|
|
9
|
+
*/
|
|
10
|
+
export interface ColorPickerLabels {
|
|
11
|
+
saturation?: string;
|
|
12
|
+
hue?: string;
|
|
13
|
+
opacity?: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
recent?: string;
|
|
16
|
+
copyHex?: string;
|
|
17
|
+
copyRgb?: string;
|
|
18
|
+
copyHsl?: string;
|
|
19
|
+
paste?: string;
|
|
20
|
+
/** Submenu title for the "show the value as…" format switch. */
|
|
21
|
+
formatAs?: string;
|
|
22
|
+
format_hex?: string;
|
|
23
|
+
format_rgb?: string;
|
|
24
|
+
format_hsl?: string;
|
|
25
|
+
/** Read out as the area's value, with {s} and {v} as whole percentages. */
|
|
26
|
+
saturationValueText?: string;
|
|
27
|
+
}
|
|
28
|
+
type __VLS_Props = {
|
|
29
|
+
/** The colour as a CSS string (v-model). Hex, rgb()/rgba() or hsl()/hsla(). */
|
|
30
|
+
modelValue?: string;
|
|
31
|
+
/** Shape of the emitted string. Parsing accepts all three either way. */
|
|
32
|
+
format?: ColorFormat;
|
|
33
|
+
/** Show the alpha rail and emit a colour carrying its alpha. */
|
|
34
|
+
alpha?: boolean;
|
|
35
|
+
/** Preset colours under the rails. Pass [] to drop the row entirely. */
|
|
36
|
+
swatches?: readonly string[];
|
|
37
|
+
/** Hide the hex/CSS text field (leaves the field, rails and presets). */
|
|
38
|
+
hideInput?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
invalid?: boolean;
|
|
41
|
+
size?: "sm" | "md" | "lg";
|
|
42
|
+
/** Trigger text beside the swatch. Defaults to the colour's own value. */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Accessible name for the trigger when it carries no visible text. */
|
|
45
|
+
ariaLabel?: string;
|
|
46
|
+
/** Popover side, as LpPopover. */
|
|
47
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
48
|
+
align?: "start" | "center" | "end";
|
|
49
|
+
/**
|
|
50
|
+
* Right-click menu on the trigger. Copy-as-hex/rgb/hsl and paste are built
|
|
51
|
+
* in — the thing a person actually wants from a colour swatch is to take
|
|
52
|
+
* the value somewhere else, and reading it off the trigger by hand is how
|
|
53
|
+
* a hex ends up mistyped. These come first; `menuItems` is appended below a
|
|
54
|
+
* separator. Pass `menu="none"` to drop the built-ins and keep only yours.
|
|
55
|
+
*/
|
|
56
|
+
menu?: "default" | "none";
|
|
57
|
+
/** Extra right-click items, appended after the built-in ones. */
|
|
58
|
+
menuItems?: ContextMenuItemDef[];
|
|
59
|
+
/**
|
|
60
|
+
* Remember the colours picked here and offer them back as a "Recent" row.
|
|
61
|
+
* A person who tints one tag reaches for the same colour on the next one,
|
|
62
|
+
* and the presets cannot know it. Off by default: history is a per-person
|
|
63
|
+
* convenience, and a component that wrote to storage uninvited would be a
|
|
64
|
+
* surprise in a form that is meant to be stateless.
|
|
65
|
+
*/
|
|
66
|
+
recent?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Storage key for that history, so two pickers can share a list or keep
|
|
69
|
+
* separate ones (e.g. "lp.color.tag" vs "lp.color.calendar").
|
|
70
|
+
*/
|
|
71
|
+
recentKey?: string;
|
|
72
|
+
/** How many recent colours to keep. */
|
|
73
|
+
recentLimit?: number;
|
|
74
|
+
/** Translations for the strings the picker renders itself. */
|
|
75
|
+
labels?: ColorPickerLabels;
|
|
76
|
+
/**
|
|
77
|
+
* Toast raised after a successful copy, with `{value}` standing in for the
|
|
78
|
+
* colour. `false` copies silently — for a page that already confirms it, or
|
|
79
|
+
* one where a toast per copy would be noise. Translate it via `labels`
|
|
80
|
+
* rather than here if the rest of your strings go through i18n.
|
|
81
|
+
*/
|
|
82
|
+
copyToast?: string | false;
|
|
83
|
+
};
|
|
84
|
+
declare var __VLS_56: {
|
|
85
|
+
color: string;
|
|
86
|
+
hsv: {
|
|
87
|
+
h: number;
|
|
88
|
+
s: number;
|
|
89
|
+
v: number;
|
|
90
|
+
a: number;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
type __VLS_Slots = {} & {
|
|
94
|
+
default?: (props: typeof __VLS_56) => any;
|
|
95
|
+
};
|
|
96
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {
|
|
97
|
+
open: import('vue').Ref<boolean, boolean>;
|
|
98
|
+
}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
99
|
+
change: (value: string) => any;
|
|
100
|
+
"update:modelValue": (value: string) => any;
|
|
101
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
102
|
+
onChange?: ((value: string) => any) | undefined;
|
|
103
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
104
|
+
}>, {
|
|
105
|
+
size: "sm" | "md" | "lg";
|
|
106
|
+
format: ColorFormat;
|
|
107
|
+
align: "start" | "center" | "end";
|
|
108
|
+
modelValue: string;
|
|
109
|
+
side: "top" | "right" | "bottom" | "left";
|
|
110
|
+
alpha: boolean;
|
|
111
|
+
swatches: readonly string[];
|
|
112
|
+
recent: boolean;
|
|
113
|
+
recentKey: string;
|
|
114
|
+
recentLimit: number;
|
|
115
|
+
copyToast: string | false;
|
|
116
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
117
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
118
|
+
declare const _default: typeof __VLS_export;
|
|
119
|
+
export default _default;
|
|
120
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
121
|
+
new (): {
|
|
122
|
+
$slots: S;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -65,18 +65,18 @@ declare const __VLS_export: import('vue').DefineComponent<__VLS_PublicProps, {},
|
|
|
65
65
|
} & {
|
|
66
66
|
clear: () => any;
|
|
67
67
|
select: (item: NotificationItem) => any;
|
|
68
|
+
dismiss: (id: string) => any;
|
|
68
69
|
markRead: (id: string) => any;
|
|
69
70
|
markUnread: (id: string) => any;
|
|
70
71
|
markAllRead: () => any;
|
|
71
|
-
dismiss: (id: string) => any;
|
|
72
72
|
}, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
73
73
|
onClear?: (() => any) | undefined;
|
|
74
74
|
onSelect?: ((item: NotificationItem) => any) | undefined;
|
|
75
75
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
76
|
+
onDismiss?: ((id: string) => any) | undefined;
|
|
76
77
|
onMarkRead?: ((id: string) => any) | undefined;
|
|
77
78
|
onMarkUnread?: ((id: string) => any) | undefined;
|
|
78
79
|
onMarkAllRead?: (() => any) | undefined;
|
|
79
|
-
onDismiss?: ((id: string) => any) | undefined;
|
|
80
80
|
}>, {
|
|
81
81
|
items: NotificationItem[];
|
|
82
82
|
maxBadge: number;
|
|
@@ -35,6 +35,26 @@ type __VLS_Props = {
|
|
|
35
35
|
* flush, header-less look.
|
|
36
36
|
*/
|
|
37
37
|
divider?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Where this nav lives.
|
|
40
|
+
*
|
|
41
|
+
* `rail` is the app shell's edge: one hairline on the inner side, square
|
|
42
|
+
* corners, the raised shell background.
|
|
43
|
+
*
|
|
44
|
+
* `panel` is a nav that sits inside a page next to the content it drives —
|
|
45
|
+
* a settings index, a table picker. It is a card: boxed on all four sides
|
|
46
|
+
* and rounded. Without it the rail's single border reads as a stray line
|
|
47
|
+
* down the middle of the page, and a consumer cannot cancel it with a class
|
|
48
|
+
* of their own.
|
|
49
|
+
*/
|
|
50
|
+
variant?: "rail" | "panel";
|
|
51
|
+
/**
|
|
52
|
+
* Rail width. A plain `class` cannot set it — the component's own `w-60`
|
|
53
|
+
* wins, and the kit does not merge conflicting Tailwind classes — so the
|
|
54
|
+
* width is a prop. Any CSS length; a page-embedded nav usually wants to
|
|
55
|
+
* match the column it lives in.
|
|
56
|
+
*/
|
|
57
|
+
width?: string;
|
|
38
58
|
/**
|
|
39
59
|
* Which collapsible groups are folded away (v-model:collapsed). Leave it
|
|
40
60
|
* unbound to let the sidebar fold long navs on its own — see
|
|
@@ -90,6 +110,7 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
90
110
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
91
111
|
"onUpdate:collapsed"?: ((keys: string[]) => any) | undefined;
|
|
92
112
|
}>, {
|
|
113
|
+
variant: "rail" | "panel";
|
|
93
114
|
skeletonRows: number;
|
|
94
115
|
skeletonHeader: boolean;
|
|
95
116
|
responsive: boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface BarEntry {
|
|
2
|
+
id: number;
|
|
3
|
+
/** Measured height in px, reported by the bar once it is on screen. */
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
/** Gap between stacked bars, matching the padding the bar sits in. */
|
|
7
|
+
export declare const STACK_GAP = 8;
|
|
8
|
+
/**
|
|
9
|
+
* Registers a bar for as long as the calling scope lives. `setHeight` reports
|
|
10
|
+
* the measured height; `offset()` returns how far up this bar should sit —
|
|
11
|
+
* the total height of every bar that opened before it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useBarStack(): {
|
|
14
|
+
register: () => void;
|
|
15
|
+
unregister: () => void;
|
|
16
|
+
setHeight: (height: number) => void;
|
|
17
|
+
offset: () => number;
|
|
18
|
+
bars: import('vue').Ref<BarEntry[] | undefined, BarEntry[] | undefined>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref, onScopeDispose } from "vue";
|
|
2
|
+
const STATE_KEY = /* @__PURE__ */ Symbol.for("@leavepulse/ui:action-bar-stack");
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
const state = g[STATE_KEY] ??= { bars: ref([]), nextId: 0 };
|
|
5
|
+
const STACK_GAP = 8;
|
|
6
|
+
function useBarStack() {
|
|
7
|
+
const id = state.nextId++;
|
|
8
|
+
const bars = state.bars;
|
|
9
|
+
function register() {
|
|
10
|
+
if (!bars.value.some((b) => b.id === id)) {
|
|
11
|
+
bars.value = [...bars.value, { id, height: 0 }];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function unregister() {
|
|
15
|
+
bars.value = bars.value.filter((b) => b.id !== id);
|
|
16
|
+
}
|
|
17
|
+
function setHeight(height) {
|
|
18
|
+
bars.value = bars.value.map((b) => b.id === id ? { ...b, height } : b);
|
|
19
|
+
}
|
|
20
|
+
function offset() {
|
|
21
|
+
const i = bars.value.findIndex((b) => b.id === id);
|
|
22
|
+
if (i <= 0) return 0;
|
|
23
|
+
return bars.value.slice(0, i).reduce((total, b) => total + b.height + STACK_GAP, 0);
|
|
24
|
+
}
|
|
25
|
+
onScopeDispose(unregister);
|
|
26
|
+
return { register, unregister, setHeight, offset, bars };
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
STACK_GAP,
|
|
30
|
+
useBarStack
|
|
31
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Hue 0..360, saturation/value 0..1, alpha 0..1. */
|
|
2
|
+
export interface Hsv {
|
|
3
|
+
h: number;
|
|
4
|
+
s: number;
|
|
5
|
+
v: number;
|
|
6
|
+
a: number;
|
|
7
|
+
}
|
|
8
|
+
/** Channels 0..255, alpha 0..1. */
|
|
9
|
+
export interface Rgb {
|
|
10
|
+
r: number;
|
|
11
|
+
g: number;
|
|
12
|
+
b: number;
|
|
13
|
+
a: number;
|
|
14
|
+
}
|
|
15
|
+
/** The string shapes a caller can hand the picker, and ask back out of it. */
|
|
16
|
+
export type ColorFormat = "hex" | "rgb" | "hsl";
|
|
17
|
+
/** Wraps rather than clamps: hue is a circle, and 370° is 10°, not 360°. */
|
|
18
|
+
export declare function normalizeHue(h: number): number;
|
|
19
|
+
export declare function clampHsv(c: Hsv): Hsv;
|
|
20
|
+
export declare function hsvToRgb({ h, s, v, a }: Hsv): Rgb;
|
|
21
|
+
export declare function rgbToHsv({ r, g, b, a }: Rgb): Hsv;
|
|
22
|
+
/** "#rrggbb", or "#rrggbbaa" when the colour is not fully opaque. */
|
|
23
|
+
export declare function rgbToHex({ r, g, b, a }: Rgb): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parses #rgb / #rgba / #rrggbb / #rrggbbaa, with or without the leading `#`.
|
|
26
|
+
* Returns null for anything it doesn't recognise, so a half-typed value in the
|
|
27
|
+
* hex field can be left alone instead of snapping the picker to black.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseHex(input: string): Rgb | null;
|
|
30
|
+
/**
|
|
31
|
+
* Parses a colour a person might paste: hex (3/4/6/8 digits, `#` optional),
|
|
32
|
+
* rgb()/rgba() and hsl()/hsla() in both the comma and the space/slash forms,
|
|
33
|
+
* percentages, hue in deg/turn/rad/grad, brackets optional, and a bare triple
|
|
34
|
+
* of numbers read as rgb. A round-trip through a caller's storage therefore
|
|
35
|
+
* comes back the colour it went in as, whatever shape it was stored in.
|
|
36
|
+
*
|
|
37
|
+
* Named colours ("rebeccapurple") and the newer spaces (lab, oklch, color())
|
|
38
|
+
* are out of scope: resolving those needs the browser's own parser, and a
|
|
39
|
+
* component that silently handled some and not others would be worse than one
|
|
40
|
+
* that is clear about what it takes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseColor(input: string): Rgb | null;
|
|
43
|
+
/** HSV→HSL, for the hsl() output format. Saturation/lightness come back 0..1. */
|
|
44
|
+
export declare function hsvToHsl({ h, s, v }: Hsv): {
|
|
45
|
+
h: number;
|
|
46
|
+
s: number;
|
|
47
|
+
l: number;
|
|
48
|
+
};
|
|
49
|
+
/** The colour as a CSS string in the requested format. */
|
|
50
|
+
export declare function formatColor(c: Hsv, format: ColorFormat): string;
|
|
51
|
+
/** Always opaque — for the swatch behind an alpha checkerboard, and for rails. */
|
|
52
|
+
export declare function opaqueCss(c: Hsv): string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether dark ink reads better than light on this colour, by WCAG relative
|
|
55
|
+
* luminance. The picker uses it for the check mark on a selected swatch, which
|
|
56
|
+
* otherwise disappears into a pale preset.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isLight(c: Hsv): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* A neutral starting palette. Kit tokens are deliberately NOT used here: these
|
|
61
|
+
* are values a user picks *for their own data* (a tag, a calendar category),
|
|
62
|
+
* which must stay put when the app's theme changes — a preset that shifted with
|
|
63
|
+
* the theme would silently restyle everything already saved with it.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DEFAULT_SWATCHES: readonly ["#ef4444", "#f97316", "#f59e0b", "#22c55e", "#10b981", "#06b6d4", "#3b82f6", "#6366f1", "#8b5cf6", "#ec4899", "#64748b", "#111827"];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
|
|
2
|
+
function normalizeHue(h) {
|
|
3
|
+
const wrapped = h % 360;
|
|
4
|
+
return wrapped < 0 ? wrapped + 360 : wrapped;
|
|
5
|
+
}
|
|
6
|
+
function clampHsv(c) {
|
|
7
|
+
return {
|
|
8
|
+
h: normalizeHue(c.h),
|
|
9
|
+
s: clamp(c.s, 0, 1),
|
|
10
|
+
v: clamp(c.v, 0, 1),
|
|
11
|
+
a: clamp(c.a, 0, 1)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function hsvToRgb({ h, s, v, a }) {
|
|
15
|
+
const hue = normalizeHue(h) / 60;
|
|
16
|
+
const c = v * s;
|
|
17
|
+
const x = c * (1 - Math.abs(hue % 2 - 1));
|
|
18
|
+
const m = v - c;
|
|
19
|
+
const [r, g, b] = [
|
|
20
|
+
[c, x, 0],
|
|
21
|
+
[x, c, 0],
|
|
22
|
+
[0, c, x],
|
|
23
|
+
[0, x, c],
|
|
24
|
+
[x, 0, c],
|
|
25
|
+
[c, 0, x]
|
|
26
|
+
][Math.floor(hue) % 6];
|
|
27
|
+
return {
|
|
28
|
+
r: Math.round((r + m) * 255),
|
|
29
|
+
g: Math.round((g + m) * 255),
|
|
30
|
+
b: Math.round((b + m) * 255),
|
|
31
|
+
a: clamp(a, 0, 1)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function rgbToHsv({ r, g, b, a }) {
|
|
35
|
+
const rn = r / 255;
|
|
36
|
+
const gn = g / 255;
|
|
37
|
+
const bn = b / 255;
|
|
38
|
+
const max = Math.max(rn, gn, bn);
|
|
39
|
+
const min = Math.min(rn, gn, bn);
|
|
40
|
+
const d = max - min;
|
|
41
|
+
let h = 0;
|
|
42
|
+
if (d !== 0) {
|
|
43
|
+
if (max === rn) h = 60 * ((gn - bn) / d % 6);
|
|
44
|
+
else if (max === gn) h = 60 * ((bn - rn) / d + 2);
|
|
45
|
+
else h = 60 * ((rn - gn) / d + 4);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
h: normalizeHue(h),
|
|
49
|
+
s: max === 0 ? 0 : d / max,
|
|
50
|
+
v: max,
|
|
51
|
+
a: clamp(a, 0, 1)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const hex2 = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
|
|
55
|
+
function rgbToHex({ r, g, b, a }) {
|
|
56
|
+
const base = `#${hex2(r)}${hex2(g)}${hex2(b)}`;
|
|
57
|
+
return a >= 1 ? base : base + hex2(a * 255);
|
|
58
|
+
}
|
|
59
|
+
function parseHex(input) {
|
|
60
|
+
const s = input.trim().replace(/^#/, "");
|
|
61
|
+
if (!/^[0-9a-f]+$/i.test(s)) return null;
|
|
62
|
+
const expand = (hex) => hex.length <= 4 ? hex.split("").map((ch) => ch + ch) : hex.match(/.{2}/g) ?? [];
|
|
63
|
+
const parts = expand(s);
|
|
64
|
+
if (s.length !== 3 && s.length !== 4 && s.length !== 6 && s.length !== 8) return null;
|
|
65
|
+
const [r, g, b, alpha] = parts.map((p) => parseInt(p, 16));
|
|
66
|
+
return { r, g, b, a: alpha === void 0 ? 1 : alpha / 255 };
|
|
67
|
+
}
|
|
68
|
+
function parseColor(input) {
|
|
69
|
+
const s = input.trim();
|
|
70
|
+
if (!s) return null;
|
|
71
|
+
if (s.startsWith("#") || /^[0-9a-f]{3,8}$/i.test(s)) return parseHex(s);
|
|
72
|
+
const fn = s.match(/^(rgba?|hsla?)\s*\(?([^)]*)\)?$/i) ?? (/^[\d.,\s/%]+$/.test(s) ? [null, "rgb", s] : null);
|
|
73
|
+
if (!fn) return null;
|
|
74
|
+
const parts = fn[2].split(/[\s,/]+/).filter(Boolean);
|
|
75
|
+
if (parts.length < 3) return null;
|
|
76
|
+
const num = (t) => parseFloat(t);
|
|
77
|
+
const angle = (t) => {
|
|
78
|
+
const n = parseFloat(t);
|
|
79
|
+
if (/turn$/i.test(t)) return n * 360;
|
|
80
|
+
if (/rad$/i.test(t)) return n * 180 / Math.PI;
|
|
81
|
+
if (/grad$/i.test(t)) return n * 0.9;
|
|
82
|
+
return n;
|
|
83
|
+
};
|
|
84
|
+
const alpha = parts[3] === void 0 ? 1 : parts[3].endsWith("%") ? num(parts[3]) / 100 : num(parts[3]);
|
|
85
|
+
if (Number.isNaN(alpha)) return null;
|
|
86
|
+
if (fn[1].toLowerCase().startsWith("rgb")) {
|
|
87
|
+
const [r, g, b] = parts.slice(0, 3).map((t) => t.endsWith("%") ? num(t) / 100 * 255 : num(t));
|
|
88
|
+
if ([r, g, b].some(Number.isNaN)) return null;
|
|
89
|
+
return { r: Math.round(clamp(r, 0, 255)), g: Math.round(clamp(g, 0, 255)), b: Math.round(clamp(b, 0, 255)), a: clamp(alpha, 0, 1) };
|
|
90
|
+
}
|
|
91
|
+
const h = angle(parts[0]);
|
|
92
|
+
const sl = num(parts[1]) / 100;
|
|
93
|
+
const l = num(parts[2]) / 100;
|
|
94
|
+
if ([h, sl, l].some(Number.isNaN)) return null;
|
|
95
|
+
return hslToRgb(h, clamp(sl, 0, 1), clamp(l, 0, 1), clamp(alpha, 0, 1));
|
|
96
|
+
}
|
|
97
|
+
function hslToRgb(h, s, l, a) {
|
|
98
|
+
const v = l + s * Math.min(l, 1 - l);
|
|
99
|
+
return hsvToRgb({ h, s: v === 0 ? 0 : 2 * (1 - l / v), v, a });
|
|
100
|
+
}
|
|
101
|
+
function hsvToHsl({ h, s, v }) {
|
|
102
|
+
const l = v * (1 - s / 2);
|
|
103
|
+
const denom = Math.min(l, 1 - l);
|
|
104
|
+
return { h: normalizeHue(h), s: denom === 0 ? 0 : (v - l) / denom, l };
|
|
105
|
+
}
|
|
106
|
+
function formatColor(c, format) {
|
|
107
|
+
const rgb = hsvToRgb(c);
|
|
108
|
+
const a = Math.round(clamp(c.a, 0, 1) * 100) / 100;
|
|
109
|
+
if (format === "hex") return rgbToHex(rgb);
|
|
110
|
+
if (format === "rgb") {
|
|
111
|
+
return a >= 1 ? `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` : `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
|
|
112
|
+
}
|
|
113
|
+
const { h, s, l } = hsvToHsl(c);
|
|
114
|
+
const hh = Math.round(h);
|
|
115
|
+
const ss = Math.round(s * 100);
|
|
116
|
+
const ll = Math.round(l * 100);
|
|
117
|
+
return a >= 1 ? `hsl(${hh}, ${ss}%, ${ll}%)` : `hsla(${hh}, ${ss}%, ${ll}%, ${a})`;
|
|
118
|
+
}
|
|
119
|
+
function opaqueCss(c) {
|
|
120
|
+
const { r, g, b } = hsvToRgb({ ...c, a: 1 });
|
|
121
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
122
|
+
}
|
|
123
|
+
function isLight(c) {
|
|
124
|
+
const { r, g, b } = hsvToRgb(c);
|
|
125
|
+
const lin = (n) => {
|
|
126
|
+
const x = n / 255;
|
|
127
|
+
return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
128
|
+
};
|
|
129
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) > 0.35;
|
|
130
|
+
}
|
|
131
|
+
const DEFAULT_SWATCHES = [
|
|
132
|
+
"#ef4444",
|
|
133
|
+
"#f97316",
|
|
134
|
+
"#f59e0b",
|
|
135
|
+
"#22c55e",
|
|
136
|
+
"#10b981",
|
|
137
|
+
"#06b6d4",
|
|
138
|
+
"#3b82f6",
|
|
139
|
+
"#6366f1",
|
|
140
|
+
"#8b5cf6",
|
|
141
|
+
"#ec4899",
|
|
142
|
+
"#64748b",
|
|
143
|
+
"#111827"
|
|
144
|
+
];
|
|
145
|
+
export {
|
|
146
|
+
DEFAULT_SWATCHES,
|
|
147
|
+
clampHsv,
|
|
148
|
+
formatColor,
|
|
149
|
+
hsvToHsl,
|
|
150
|
+
hsvToRgb,
|
|
151
|
+
isLight,
|
|
152
|
+
normalizeHue,
|
|
153
|
+
opaqueCss,
|
|
154
|
+
parseColor,
|
|
155
|
+
parseHex,
|
|
156
|
+
rgbToHex,
|
|
157
|
+
rgbToHsv
|
|
158
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface CopyOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Toast to raise on success. `false` (the default) stays silent, for callers
|
|
4
|
+
* whose button already shows the tick and would otherwise say it twice.
|
|
5
|
+
*/
|
|
6
|
+
toast?: string | false;
|
|
7
|
+
/**
|
|
8
|
+
* Toast to raise when the copy fails. Defaults to silence for the same
|
|
9
|
+
* reason the kit's other best-effort paths are silent: a copy that didn't
|
|
10
|
+
* happen is usually obvious, and a scary red box over a blocked clipboard
|
|
11
|
+
* helps nobody. Pass a string where losing the copy actually costs something.
|
|
12
|
+
*/
|
|
13
|
+
errorToast?: string | false;
|
|
14
|
+
}
|
|
15
|
+
export interface UseClipboard {
|
|
16
|
+
/**
|
|
17
|
+
* True for a moment after a successful copy. Bind it to swap a copy icon for
|
|
18
|
+
* a tick:
|
|
19
|
+
*
|
|
20
|
+
* ```vue
|
|
21
|
+
* <LpIcon :name="copied ? 'lucide:check' : 'lucide:copy'" />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
copied: import('vue').Ref<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Writes `text` to the clipboard. Resolves true on success, false when the
|
|
27
|
+
* clipboard was unavailable or refused — it never rejects, so `await copy(x)`
|
|
28
|
+
* is safe to call bare from an event handler.
|
|
29
|
+
*/
|
|
30
|
+
copy: (text: string, options?: CopyOptions) => Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Clipboard writes with optional toast feedback.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const { copy, copied } = useClipboard()
|
|
37
|
+
* copy(hex) // silent; bind `copied` to a tick
|
|
38
|
+
* copy(hex, { toast: "Copied #3b82f6" }) // says so
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Defaults are per-call rather than per-instance: one component often copies
|
|
42
|
+
* several things for different reasons — a menu item that should announce
|
|
43
|
+
* itself, a button that shows its own tick — and hoisting the choice to the
|
|
44
|
+
* composable would force those to agree.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useClipboard(): UseClipboard;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ref, onScopeDispose } from "vue";
|
|
2
|
+
import { useToast } from "./useToast.js";
|
|
3
|
+
const COPIED_MS = 1400;
|
|
4
|
+
function useClipboard() {
|
|
5
|
+
const copied = ref(false);
|
|
6
|
+
const { success, error } = useToast();
|
|
7
|
+
let timer;
|
|
8
|
+
async function copy(text, options = {}) {
|
|
9
|
+
const { toast = false, errorToast = false } = options;
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard?.writeText(text);
|
|
12
|
+
if (!navigator.clipboard) throw new Error("clipboard unavailable");
|
|
13
|
+
copied.value = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
timer = setTimeout(() => copied.value = false, COPIED_MS);
|
|
16
|
+
if (toast) success(toast);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
if (errorToast) error(errorToast);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
onScopeDispose(() => clearTimeout(timer));
|
|
24
|
+
return { copied, copy };
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
useClipboard
|
|
28
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { default as LpAddressInput } from './components/LpAddressInput.vue';
|
|
|
3
3
|
export { default as LpMapPicker } from './components/LpMapPicker.vue';
|
|
4
4
|
export { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject, } from './components/address';
|
|
5
5
|
export type { AddressField, AddressLabels, AddressSuggestion, AddressValue, GeocodeProvider, GeocodeQuery, LatLon, NominatimOptions, TileSource, } from './components/address';
|
|
6
|
+
export { default as LpActionBar } from './components/LpActionBar.vue';
|
|
7
|
+
export type { ActionBarAction } from './components/LpActionBar.vue';
|
|
6
8
|
export { default as LpAlert } from './components/LpAlert.vue';
|
|
7
9
|
export { default as LpAppShell } from './components/LpAppShell.vue';
|
|
8
10
|
export { default as LpAutocomplete } from './components/LpAutocomplete.vue';
|
|
@@ -21,6 +23,10 @@ export type { ChartAxis, ChartPoint, ChartSeries } from './components/chart';
|
|
|
21
23
|
export { default as LpCheckbox } from './components/LpCheckbox.vue';
|
|
22
24
|
export { default as LpCodeBlock } from './components/LpCodeBlock.vue';
|
|
23
25
|
export type { CodeLang } from './components/codeHighlight';
|
|
26
|
+
export { default as LpColorPicker } from './components/LpColorPicker.vue';
|
|
27
|
+
export type { ColorPickerLabels } from './components/LpColorPicker.vue';
|
|
28
|
+
export { clampHsv, DEFAULT_SWATCHES, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv, } from './components/color';
|
|
29
|
+
export type { ColorFormat, Hsv, Rgb } from './components/color';
|
|
24
30
|
export { default as LpCommandPalette } from './components/LpCommandPalette.vue';
|
|
25
31
|
export type { Command } from './components/LpCommandPalette.vue';
|
|
26
32
|
export { default as LpConfirmDialog } from './components/LpConfirmDialog.vue';
|
|
@@ -124,6 +130,8 @@ export { amber, dark, leavepulse, light, lime, nord, presets, rose, violet } fro
|
|
|
124
130
|
export type { PresetName } from './theme/presets';
|
|
125
131
|
export { parseConfig, serializeConfig, UI_CONFIG_VERSION } from './theme/config';
|
|
126
132
|
export type { UiConfig } from './theme/config';
|
|
133
|
+
export { useClipboard } from './composables/useClipboard';
|
|
134
|
+
export type { CopyOptions, UseClipboard } from './composables/useClipboard';
|
|
127
135
|
export { useHotkeys, isAppleKeyboard, appleModifierLabel } from './composables/useHotkeys';
|
|
128
136
|
export type { HotkeyBinding } from './composables/useHotkeys';
|
|
129
137
|
export { useInputFilter } from './composables/useInputFilter';
|