@adea-ai/ui 0.142.4 → 0.144.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/components/composites/accent-picker/accent-picker.d.ts +28 -0
- package/dist/components/composites/accent-picker/accent-picker.d.ts.map +1 -0
- package/dist/components/composites/accent-picker/accent-picker.js +107 -0
- package/dist/components/composites/accent-picker/accent-picker.js.map +1 -0
- package/dist/components/composites/accent-picker/index.d.ts +2 -0
- package/dist/components/composites/accent-picker/index.d.ts.map +1 -0
- package/dist/components/composites/accent-picker/index.js +2 -0
- package/dist/components/composites/icon-picker/icon-picker.d.ts +52 -0
- package/dist/components/composites/icon-picker/icon-picker.d.ts.map +1 -0
- package/dist/components/composites/icon-picker/icon-picker.js +265 -0
- package/dist/components/composites/icon-picker/icon-picker.js.map +1 -0
- package/dist/components/composites/icon-picker/image-data-url.d.ts +42 -0
- package/dist/components/composites/icon-picker/image-data-url.d.ts.map +1 -0
- package/dist/components/composites/icon-picker/image-data-url.js +90 -0
- package/dist/components/composites/icon-picker/image-data-url.js.map +1 -0
- package/dist/components/composites/icon-picker/index.d.ts +4 -0
- package/dist/components/composites/icon-picker/index.d.ts.map +1 -0
- package/dist/components/composites/icon-picker/index.js +4 -0
- package/dist/components/composites/icon-picker/shell-icons.d.ts +264 -0
- package/dist/components/composites/icon-picker/shell-icons.d.ts.map +1 -0
- package/dist/components/composites/icon-picker/shell-icons.js +334 -0
- package/dist/components/composites/icon-picker/shell-icons.js.map +1 -0
- package/dist/components/composites/settings/settings-navigation.js +1 -1
- package/dist/components/composites/settings/settings-navigation.js.map +1 -1
- package/dist/components/composites/settings/settings.d.ts +8 -14
- package/dist/components/composites/settings/settings.d.ts.map +1 -1
- package/dist/components/composites/settings/settings.js +88 -37
- package/dist/components/composites/settings/settings.js.map +1 -1
- package/dist/components/composites/terminal/terminal.js +2 -2
- package/dist/components/composites/terminal/terminal.js.map +1 -1
- package/dist/components/composites/tree/tree.d.ts +6 -0
- package/dist/components/composites/tree/tree.d.ts.map +1 -1
- package/dist/components/composites/tree/tree.js +35 -14
- package/dist/components/composites/tree/tree.js.map +1 -1
- package/dist/components/composites/workspace-switcher/index.d.ts +2 -0
- package/dist/components/composites/workspace-switcher/index.d.ts.map +1 -0
- package/dist/components/composites/workspace-switcher/index.js +2 -0
- package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts +44 -0
- package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts.map +1 -0
- package/dist/components/composites/workspace-switcher/workspace-switcher.js +157 -0
- package/dist/components/composites/workspace-switcher/workspace-switcher.js.map +1 -0
- package/dist/components/layout/shell-rail/index.d.ts +2 -0
- package/dist/components/layout/shell-rail/index.d.ts.map +1 -0
- package/dist/components/layout/shell-rail/index.js +2 -0
- package/dist/components/layout/shell-rail/shell-rail.d.ts +36 -0
- package/dist/components/layout/shell-rail/shell-rail.d.ts.map +1 -0
- package/dist/components/layout/shell-rail/shell-rail.js +124 -0
- package/dist/components/layout/shell-rail/shell-rail.js.map +1 -0
- package/dist/components/layout/shell-top-bar/index.d.ts +2 -0
- package/dist/components/layout/shell-top-bar/index.d.ts.map +1 -0
- package/dist/components/layout/shell-top-bar/index.js +2 -0
- package/dist/components/layout/shell-top-bar/shell-top-bar.d.ts +68 -0
- package/dist/components/layout/shell-top-bar/shell-top-bar.d.ts.map +1 -0
- package/dist/components/layout/shell-top-bar/shell-top-bar.js +135 -0
- package/dist/components/layout/shell-top-bar/shell-top-bar.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/r/accent-picker.json +25 -0
- package/dist/r/icon-picker.json +37 -0
- package/dist/r/registry.json +141 -1
- package/dist/r/settings.json +3 -1
- package/dist/r/shell-rail.json +27 -0
- package/dist/r/shell-top-bar.json +23 -0
- package/dist/r/src/components/composites/accent-picker/accent-picker.tsx +106 -0
- package/dist/r/src/components/composites/accent-picker/index.ts +1 -0
- package/dist/r/src/components/composites/icon-picker/icon-picker.tsx +246 -0
- package/dist/r/src/components/composites/icon-picker/image-data-url.ts +107 -0
- package/dist/r/src/components/composites/icon-picker/index.ts +25 -0
- package/dist/r/src/components/composites/icon-picker/shell-icons.ts +166 -0
- package/dist/r/src/components/composites/settings/settings-navigation.tsx +1 -1
- package/dist/r/src/components/composites/settings/settings.tsx +40 -5
- package/dist/r/src/components/composites/tree/tree.tsx +25 -0
- package/dist/r/src/components/composites/workspace-switcher/index.ts +5 -0
- package/dist/r/src/components/composites/workspace-switcher/workspace-switcher.tsx +128 -0
- package/dist/r/src/components/layout/shell-rail/index.ts +1 -0
- package/dist/r/src/components/layout/shell-rail/shell-rail.tsx +120 -0
- package/dist/r/src/components/layout/shell-top-bar/index.ts +1 -0
- package/dist/r/src/components/layout/shell-top-bar/shell-top-bar.tsx +185 -0
- package/dist/r/workspace-switcher.json +26 -0
- package/package.json +1 -1
- package/registry.json +141 -1
- package/src/components/composites/accent-picker/accent-picker.tsx +106 -0
- package/src/components/composites/accent-picker/index.ts +1 -0
- package/src/components/composites/icon-picker/icon-picker.tsx +246 -0
- package/src/components/composites/icon-picker/image-data-url.ts +107 -0
- package/src/components/composites/icon-picker/index.ts +25 -0
- package/src/components/composites/icon-picker/shell-icons.ts +166 -0
- package/src/components/composites/settings/settings-navigation.tsx +1 -1
- package/src/components/composites/settings/settings.tsx +40 -5
- package/src/components/composites/tree/tree.tsx +25 -0
- package/src/components/composites/workspace-switcher/index.ts +5 -0
- package/src/components/composites/workspace-switcher/workspace-switcher.tsx +128 -0
- package/src/components/layout/shell-rail/index.ts +1 -0
- package/src/components/layout/shell-rail/shell-rail.tsx +120 -0
- package/src/components/layout/shell-top-bar/index.ts +1 -0
- package/src/components/layout/shell-top-bar/shell-top-bar.tsx +185 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js';
|
|
2
|
+
/**
|
|
3
|
+
* AccentPicker.
|
|
4
|
+
*
|
|
5
|
+
* A row of accent swatches with a leading "none" choice, for a setting that
|
|
6
|
+
* overrides the accent it would otherwise inherit: a project's icon colour, a
|
|
7
|
+
* workspace's colour over the app's. `null` is a real value, not an empty one:
|
|
8
|
+
* it means "do not override", so the swatch row never has an unchecked state and
|
|
9
|
+
* the control can say what the inherited colour is through `inheritLabel`.
|
|
10
|
+
*
|
|
11
|
+
* The swatches are the theme's own accent presets, each carrying its light and
|
|
12
|
+
* dark colour and switching on the `dark` class, so a swatch is the colour the
|
|
13
|
+
* setting will produce in whichever appearance is in force and the picker needs
|
|
14
|
+
* no theme provider.
|
|
15
|
+
*/
|
|
16
|
+
export type AccentPickerProps = {
|
|
17
|
+
/** The group's accessible name, e.g. "Icon colour". */
|
|
18
|
+
label: string;
|
|
19
|
+
/** An accent preset id, or `null` for no override. */
|
|
20
|
+
value: string | null;
|
|
21
|
+
onValueChange(value: string | null): void;
|
|
22
|
+
/** What `null` means here, e.g. "Inherit workspace accent". Also the first swatch's name and tooltip. */
|
|
23
|
+
inheritLabel?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
class?: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function AccentPicker(props: AccentPickerProps): JSX.Element;
|
|
28
|
+
//# sourceMappingURL=accent-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accent-picker.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/accent-picker/accent-picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAMnC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IACzC,yGAAyG;IACzG,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,eA6BpD"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip/tooltip.js";
|
|
3
|
+
import { accents } from "../../../lib/accent-presets.js";
|
|
4
|
+
import { createComponent, effect, insert, memo, setStyleProperty, template } from "solid-js/web";
|
|
5
|
+
import { For } from "solid-js";
|
|
6
|
+
import { RadioGroup } from "@kobalte/core/radio-group";
|
|
7
|
+
//#region src/components/composites/accent-picker/accent-picker.tsx
|
|
8
|
+
var _tmpl$ = /*#__PURE__*/ template(`<span aria-hidden=true class="block size-full rounded-full bg-(--swatch-light) dark:bg-(--swatch-dark)">`);
|
|
9
|
+
var _tmpl$2 = /*#__PURE__*/ template(`<span aria-hidden=true data-slot=swatch-inherit class="relative block size-full overflow-hidden rounded-full bg-(--swatch-inherit)"style=--swatch-inherit:#000><span class="absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 -rotate-45 bg-destructive">`);
|
|
10
|
+
var _tmpl$3 = /*#__PURE__*/ template(`<span class=sr-only>`);
|
|
11
|
+
var NONE = "__none__";
|
|
12
|
+
function AccentPicker(props) {
|
|
13
|
+
return createComponent(RadioGroup, {
|
|
14
|
+
"data-slot": "swatch-picker",
|
|
15
|
+
get ["aria-label"]() {
|
|
16
|
+
return props.label;
|
|
17
|
+
},
|
|
18
|
+
get value() {
|
|
19
|
+
return props.value ?? NONE;
|
|
20
|
+
},
|
|
21
|
+
get disabled() {
|
|
22
|
+
return props.disabled;
|
|
23
|
+
},
|
|
24
|
+
onChange: (id) => props.onValueChange(id === NONE ? null : id),
|
|
25
|
+
get ["class"]() {
|
|
26
|
+
return cn("flex flex-wrap items-center gap-2", props.class);
|
|
27
|
+
},
|
|
28
|
+
get children() {
|
|
29
|
+
return [createComponent(SwatchItem, {
|
|
30
|
+
id: NONE,
|
|
31
|
+
get label() {
|
|
32
|
+
return props.inheritLabel ?? "Inherit (default)";
|
|
33
|
+
},
|
|
34
|
+
get children() {
|
|
35
|
+
return createComponent(InheritSwatch, {});
|
|
36
|
+
}
|
|
37
|
+
}), createComponent(For, {
|
|
38
|
+
each: accents,
|
|
39
|
+
children: (accent) => createComponent(SwatchItem, {
|
|
40
|
+
get id() {
|
|
41
|
+
return accent.id;
|
|
42
|
+
},
|
|
43
|
+
get label() {
|
|
44
|
+
return accent.label;
|
|
45
|
+
},
|
|
46
|
+
get children() {
|
|
47
|
+
var _el$ = _tmpl$();
|
|
48
|
+
effect((_p$) => {
|
|
49
|
+
var _v$ = accent.light ?? "currentColor", _v$2 = accent.dark ?? accent.light ?? "currentColor";
|
|
50
|
+
_v$ !== _p$.e && setStyleProperty(_el$, "--swatch-light", _p$.e = _v$);
|
|
51
|
+
_v$2 !== _p$.t && setStyleProperty(_el$, "--swatch-dark", _p$.t = _v$2);
|
|
52
|
+
return _p$;
|
|
53
|
+
}, {
|
|
54
|
+
e: void 0,
|
|
55
|
+
t: void 0
|
|
56
|
+
});
|
|
57
|
+
return _el$;
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})];
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The "inherit" swatch: a black disc with a red slash, the universal "no
|
|
66
|
+
* override" mark. It is drawn rather than painted from a token so it reads the
|
|
67
|
+
* same in every theme and appearance.
|
|
68
|
+
*/
|
|
69
|
+
function InheritSwatch() {
|
|
70
|
+
return _tmpl$2();
|
|
71
|
+
}
|
|
72
|
+
function SwatchItem(props) {
|
|
73
|
+
return createComponent(RadioGroup.Item, {
|
|
74
|
+
get value() {
|
|
75
|
+
return props.id;
|
|
76
|
+
},
|
|
77
|
+
get ["data-accent-option"]() {
|
|
78
|
+
return props.id;
|
|
79
|
+
},
|
|
80
|
+
"class": "contents",
|
|
81
|
+
get children() {
|
|
82
|
+
return [createComponent(RadioGroup.ItemInput, { "class": "peer" }), createComponent(Tooltip, { get children() {
|
|
83
|
+
return [createComponent(TooltipTrigger, {
|
|
84
|
+
get as() {
|
|
85
|
+
return RadioGroup.ItemLabel;
|
|
86
|
+
},
|
|
87
|
+
get ["class"]() {
|
|
88
|
+
return cn("flex size-7 cursor-pointer items-center justify-center rounded-full border border-border p-0.5", "peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle", "data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50");
|
|
89
|
+
},
|
|
90
|
+
get children() {
|
|
91
|
+
return [memo(() => props.children), (() => {
|
|
92
|
+
var _el$3 = _tmpl$3();
|
|
93
|
+
insert(_el$3, () => props.label);
|
|
94
|
+
return _el$3;
|
|
95
|
+
})()];
|
|
96
|
+
}
|
|
97
|
+
}), createComponent(TooltipContent, { get children() {
|
|
98
|
+
return props.label;
|
|
99
|
+
} })];
|
|
100
|
+
} })];
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { AccentPicker };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=accent-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accent-picker.js","names":["RadioGroup","Radio","JSX","For","accents","cn","Tooltip","TooltipContent","TooltipTrigger","AccentPickerProps","label","value","onValueChange","inheritLabel","disabled","class","NONE","AccentPicker","props","_$createComponent","aria-label","onChange","id","children","SwatchItem","InheritSwatch","each","accent","_el$","_tmpl$","_$effect","_p$","_v$","light","_v$2","dark","e","_$setStyleProperty","t","undefined","_tmpl$2","Element","Item","data-accent-option","ItemInput","as","ItemLabel","_$memo","_el$3","_tmpl$3","_$insert","_$createComponent","Radio","_$setStyleProperty","_$memo"],"sources":["../../../../src/components/composites/accent-picker/accent-picker.tsx"],"sourcesContent":["import { RadioGroup as Radio } from '@kobalte/core/radio-group'\nimport type { JSX } from 'solid-js'\nimport { For } from 'solid-js'\nimport { accents } from '#lib/accent-presets'\nimport { cn } from '#lib/utils'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'\n\n/**\n * AccentPicker.\n *\n * A row of accent swatches with a leading \"none\" choice, for a setting that\n * overrides the accent it would otherwise inherit: a project's icon colour, a\n * workspace's colour over the app's. `null` is a real value, not an empty one:\n * it means \"do not override\", so the swatch row never has an unchecked state and\n * the control can say what the inherited colour is through `inheritLabel`.\n *\n * The swatches are the theme's own accent presets, each carrying its light and\n * dark colour and switching on the `dark` class, so a swatch is the colour the\n * setting will produce in whichever appearance is in force and the picker needs\n * no theme provider.\n */\nexport type AccentPickerProps = {\n /** The group's accessible name, e.g. \"Icon colour\". */\n label: string\n /** An accent preset id, or `null` for no override. */\n value: string | null\n onValueChange(value: string | null): void\n /** What `null` means here, e.g. \"Inherit workspace accent\". Also the first swatch's name and tooltip. */\n inheritLabel?: string\n disabled?: boolean\n class?: string\n}\n\nconst NONE = '__none__'\n\nexport function AccentPicker(props: AccentPickerProps) {\n return (\n <Radio\n data-slot=\"swatch-picker\"\n aria-label={props.label}\n value={props.value ?? NONE}\n disabled={props.disabled}\n onChange={(id) => props.onValueChange(id === NONE ? null : id)}\n class={cn('flex flex-wrap items-center gap-2', props.class)}\n >\n <SwatchItem id={NONE} label={props.inheritLabel ?? 'Inherit (default)'}>\n <InheritSwatch />\n </SwatchItem>\n <For each={accents}>\n {(accent) => (\n <SwatchItem id={accent.id} label={accent.label}>\n <span\n aria-hidden=\"true\"\n class=\"block size-full rounded-full bg-(--swatch-light) dark:bg-(--swatch-dark)\"\n style={{\n '--swatch-light': accent.light ?? 'currentColor',\n '--swatch-dark': accent.dark ?? accent.light ?? 'currentColor',\n }}\n />\n </SwatchItem>\n )}\n </For>\n </Radio>\n )\n}\n\n/**\n * The \"inherit\" swatch: a black disc with a red slash, the universal \"no\n * override\" mark. It is drawn rather than painted from a token so it reads the\n * same in every theme and appearance.\n */\nfunction InheritSwatch() {\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"swatch-inherit\"\n class=\"relative block size-full overflow-hidden rounded-full bg-(--swatch-inherit)\"\n style={{ '--swatch-inherit': '#000' }}\n >\n <span class=\"absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 -rotate-45 bg-destructive\" />\n </span>\n )\n}\n\nfunction SwatchItem(props: { id: string; label: string; children: JSX.Element }) {\n return (\n <Radio.Item value={props.id} data-accent-option={props.id} class=\"contents\">\n <Radio.ItemInput class=\"peer\" />\n <Tooltip>\n <TooltipTrigger\n as={Radio.ItemLabel}\n class={cn(\n 'flex size-7 cursor-pointer items-center justify-center rounded-full border border-border p-0.5',\n 'peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle',\n 'data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50'\n )}\n >\n {props.children}\n <span class=\"sr-only\">{props.label}</span>\n </TooltipTrigger>\n <TooltipContent>{props.label}</TooltipContent>\n </Tooltip>\n </Radio.Item>\n )\n}\n"],"mappings":";;;;;;;;;;AAiCA,IAAMgB,OAAO;AAEb,SAAgBC,aAAaC,OAA0B;CACrD,OAAAC,gBACGlB,YAAK;EAAA,aAAA;EAAA,KAAA,gBAAA;GAAA,OAEQiB,MAAMR;EAAK;EAAA,IACvBC,QAAK;GAAA,OAAEO,MAAMP,SAASK;EAAI;EAAA,IAC1BF,WAAQ;GAAA,OAAEI,MAAMJ;EAAQ;EACxBO,WAAWC,OAAOJ,MAAMN,cAAcU,OAAON,OAAO,OAAOM,EAAE;EAAC,KAAA,WAAA;GAAA,OACvDjB,GAAG,qCAAqCa,MAAMH,KAAK;EAAC;EAAA,IAAAQ,WAAA;GAAA,OAAA,CAAAJ,gBAE1DK,YAAU;IAACF,IAAIN;IAAI,IAAEN,QAAK;KAAA,OAAEQ,MAAML,gBAAgB;IAAmB;IAAA,IAAAU,WAAA;KAAA,OAAAJ,gBACnEM,eAAa,CAAA,CAAA;IAAA;GAAA,CAAA,GAAAN,gBAEfhB,KAAG;IAACuB,MAAMtB;IAAOmB,WACdI,WAAMR,gBACLK,YAAU;KAAA,IAACF,KAAE;MAAA,OAAEK,OAAOL;KAAE;KAAA,IAAEZ,QAAK;MAAA,OAAEiB,OAAOjB;KAAK;KAAA,IAAAa,WAAA;MAAA,IAAAK,OAAAC,OAAA;MAAAC,QAAAC,QAAA;OAAA,IAAAC,MAKtBL,OAAOM,SAAS,gBAAcC,OAC/BP,OAAOQ,QAAQR,OAAOM,SAAS;OAAcD,QAAAD,IAAAK,KAAAC,iBAAAT,MAAA,kBAAAG,IAAAK,IAAAJ,GAAA;OAAAE,SAAAH,IAAAO,KAAAD,iBAAAT,MAAA,iBAAAG,IAAAO,IAAAJ,IAAA;OAAA,OAAAH;MAAA,GAAA;OAAAK,GAAAG,KAAAA;OAAAD,GAAAC,KAAAA;MAAA,CAAA;MAAA,OAAAX;KAAA;IAAA,CAAA;GAIrE,CAAA,CAAA;EAAA;CAAA,CAAA;AAIT;;;;;;AAOA,SAASH,gBAAgB;CACvB,OAAAe,QAAA;AAUF;AAEA,SAAShB,WAAWN,OAA6D;CAC/E,OAAAC,gBACGlB,WAAMyC,MAAI;EAAA,IAAC/B,QAAK;GAAA,OAAEO,MAAMI;EAAE;EAAA,KAAA,wBAAA;GAAA,OAAsBJ,MAAMI;EAAE;EAAA,SAAA;EAAA,IAAAC,WAAA;GAAA,OAAA,CAAAJ,gBACtDlB,WAAM2C,WAAS,EAAA,SAAA,OAAA,CAAA,GAAAzB,gBACfb,SAAO,EAAA,IAAAiB,WAAA;IAAA,OAAA,CAAAJ,gBACLX,gBAAc;KAAA,IACbqC,KAAE;MAAA,OAAE5C,WAAM6C;KAAS;KAAA,KAAA,WAAA;MAAA,OACZzC,GACL,kGACA,mGACA,mFACA,gEACF;KAAC;KAAA,IAAAkB,WAAA;MAAA,OAAA,CAAAwB,WAEA7B,MAAMK,QAAQ,UAAA;OAAA,IAAAyB,QAAAC,QAAA;OAAAC,OAAAF,aACQ9B,MAAMR,KAAK;OAAA,OAAAsC;MAAA,EAAA,CAAA,CAAA;KAAA;IAAA,CAAA,GAAA7B,gBAEnCZ,gBAAc,EAAA,IAAAgB,WAAA;KAAA,OAAEL,MAAMR;IAAK,EAAA,CAAA,CAAA;GAAA,EAAA,CAAA,CAAA;EAAA;CAAA,CAAA;AAIpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/accent-picker/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js';
|
|
2
|
+
/**
|
|
3
|
+
* IconPicker.
|
|
4
|
+
*
|
|
5
|
+
* The control behind "choose an icon" for a workspace or a project: a grid of
|
|
6
|
+
* curated glyphs, and optionally an uploaded picture in their place.
|
|
7
|
+
*
|
|
8
|
+
* It is a single-choice group. The glyph grid is a Kobalte radio group, so
|
|
9
|
+
* arrow keys move the choice and Tab enters and leaves the grid as one stop,
|
|
10
|
+
* which is the contract a 56-cell grid needs to stay usable from a keyboard. An
|
|
11
|
+
* uploaded image is a separate state, not a 57th cell: while `image` is set no
|
|
12
|
+
* glyph is checked, and choosing a glyph asks the caller to drop the image, so
|
|
13
|
+
* the stored mark is never both.
|
|
14
|
+
*
|
|
15
|
+
* The picker does not persist anything. The caller owns `value` and `image`
|
|
16
|
+
* and saves them with the rest of its form; the image arrives already resized
|
|
17
|
+
* to the stored square (see `markImageFromFile`).
|
|
18
|
+
*/
|
|
19
|
+
export type IconPickerProps = {
|
|
20
|
+
/** The group's accessible name, e.g. "Workspace icon". */
|
|
21
|
+
label: string;
|
|
22
|
+
/** The checked icon key; ignored while `image` is set. */
|
|
23
|
+
value: string | undefined;
|
|
24
|
+
onValueChange(key: string): void;
|
|
25
|
+
/** The key the surface treats as unset, labelled "default" in the grid. */
|
|
26
|
+
defaultKey?: string;
|
|
27
|
+
/** An uploaded mark (a data URL) currently chosen in place of a glyph. */
|
|
28
|
+
image?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Called with the resized upload, or `undefined` for Remove. Omit it for a
|
|
31
|
+
* surface that offers glyphs only; the upload row then does not render.
|
|
32
|
+
*/
|
|
33
|
+
onImageChange?(image: string | undefined): void;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
class?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function IconPicker(props: IconPickerProps): JSX.Element;
|
|
38
|
+
/**
|
|
39
|
+
* IconPickerDropdown.
|
|
40
|
+
*
|
|
41
|
+
* The compact form of the picker for a settings row: one button showing the
|
|
42
|
+
* current mark, opening the full grid (and the upload row, when the host offers
|
|
43
|
+
* it) in a popover. Choosing a glyph applies it and closes the popover; an
|
|
44
|
+
* upload or Remove leaves it open so the result is visible. The host owns the
|
|
45
|
+
* value exactly as it does for `IconPicker`.
|
|
46
|
+
*/
|
|
47
|
+
export type IconPickerDropdownProps = IconPickerProps & {
|
|
48
|
+
/** The mark shown on the trigger; defaults to the checked glyph or the uploaded image. */
|
|
49
|
+
trigger?: JSX.Element;
|
|
50
|
+
};
|
|
51
|
+
export declare function IconPickerDropdown(props: IconPickerDropdownProps): JSX.Element;
|
|
52
|
+
//# sourceMappingURL=icon-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-picker.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/icon-picker/icon-picker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAcnC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,aAAa,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,eAsIhD;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG;IACtD,0FAA0F;IAC1F,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;CACtB,CAAA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,eA2ChE"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { Button } from "../../ui/button/button.js";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover/popover.js";
|
|
4
|
+
import { MARK_IMAGE_MAX_SOURCE_BYTES, MarkImageError, markImageFromFile, markImageProblemCopy } from "./image-data-url.js";
|
|
5
|
+
import { resolveShellIcon, shellIconEntries, shellIconLabel } from "./shell-icons.js";
|
|
6
|
+
import { Dynamic, className, createComponent, effect, insert, memo, mergeProps, setAttribute, template, use } from "solid-js/web";
|
|
7
|
+
import { ChevronDown, ImageUp, Trash2 } from "lucide-solid";
|
|
8
|
+
import { For, Show, createSignal, createUniqueId, splitProps } from "solid-js";
|
|
9
|
+
import { RadioGroup } from "@kobalte/core/radio-group";
|
|
10
|
+
//#region src/components/composites/icon-picker/icon-picker.tsx
|
|
11
|
+
var _tmpl$ = /*#__PURE__*/ template(`<div class="flex items-center gap-3"data-slot=icon-picker-upload><span aria-hidden=true class="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-muted"></span><div class="flex flex-wrap items-center gap-2"><input type=file class=sr-only tabindex=-1 aria-hidden=true accept=image/png,image/jpeg,image/webp,image/gif>`);
|
|
12
|
+
var _tmpl$2 = /*#__PURE__*/ template(`<p class="text-xs text-muted-foreground">PNG, JPEG, WebP or GIF up to <!> MB. It is cropped to a square.`);
|
|
13
|
+
var _tmpl$3 = /*#__PURE__*/ template(`<div data-slot=icon-picker>`);
|
|
14
|
+
var _tmpl$4 = /*#__PURE__*/ template(`<img alt class="size-full object-cover">`);
|
|
15
|
+
var _tmpl$5 = /*#__PURE__*/ template(`<p role=alert class="text-sm text-destructive">`);
|
|
16
|
+
var _tmpl$6 = /*#__PURE__*/ template(`<span class=sr-only>`);
|
|
17
|
+
var _tmpl$7 = /*#__PURE__*/ template(`<span aria-hidden=true class="flex size-5 items-center justify-center overflow-hidden">`);
|
|
18
|
+
var _tmpl$8 = /*#__PURE__*/ template(`<img alt class="size-full rounded-sm object-cover">`);
|
|
19
|
+
function IconPicker(props) {
|
|
20
|
+
const inputId = createUniqueId();
|
|
21
|
+
const [problem, setProblem] = createSignal();
|
|
22
|
+
const [busy, setBusy] = createSignal(false);
|
|
23
|
+
const [fileInput, setFileInput] = createSignal();
|
|
24
|
+
const upload = async (file) => {
|
|
25
|
+
if (!file || !props.onImageChange) return;
|
|
26
|
+
setProblem(void 0);
|
|
27
|
+
setBusy(true);
|
|
28
|
+
try {
|
|
29
|
+
props.onImageChange(await markImageFromFile(file));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
setProblem(markImageProblemCopy[error instanceof MarkImageError ? error.problem : "unreadable"]);
|
|
32
|
+
} finally {
|
|
33
|
+
setBusy(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return (() => {
|
|
37
|
+
var _el$ = _tmpl$3();
|
|
38
|
+
insert(_el$, createComponent(Show, {
|
|
39
|
+
get when() {
|
|
40
|
+
return props.onImageChange;
|
|
41
|
+
},
|
|
42
|
+
get children() {
|
|
43
|
+
return [
|
|
44
|
+
(() => {
|
|
45
|
+
var _el$2 = _tmpl$(), _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling, _el$5 = _el$4.firstChild;
|
|
46
|
+
insert(_el$3, createComponent(Show, {
|
|
47
|
+
get when() {
|
|
48
|
+
return props.image;
|
|
49
|
+
},
|
|
50
|
+
get fallback() {
|
|
51
|
+
return createComponent(ImageUp, {
|
|
52
|
+
"class": "size-5 text-muted-foreground",
|
|
53
|
+
"aria-hidden": "true"
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
children: (src) => (() => {
|
|
57
|
+
var _el$0 = _tmpl$4();
|
|
58
|
+
effect(() => setAttribute(_el$0, "src", src()));
|
|
59
|
+
return _el$0;
|
|
60
|
+
})()
|
|
61
|
+
}));
|
|
62
|
+
_el$5.addEventListener("change", (event) => {
|
|
63
|
+
const input = event.currentTarget;
|
|
64
|
+
upload(input.files?.[0]);
|
|
65
|
+
input.value = "";
|
|
66
|
+
});
|
|
67
|
+
use(setFileInput, _el$5);
|
|
68
|
+
setAttribute(_el$5, "id", inputId);
|
|
69
|
+
insert(_el$4, createComponent(Button, {
|
|
70
|
+
type: "button",
|
|
71
|
+
variant: "outline",
|
|
72
|
+
size: "sm",
|
|
73
|
+
get disabled() {
|
|
74
|
+
return props.disabled || busy();
|
|
75
|
+
},
|
|
76
|
+
onClick: () => fileInput()?.click(),
|
|
77
|
+
get children() {
|
|
78
|
+
return [createComponent(ImageUp, { "aria-hidden": "true" }), memo(() => props.image ? "Replace image" : "Upload image")];
|
|
79
|
+
}
|
|
80
|
+
}), null);
|
|
81
|
+
insert(_el$4, createComponent(Show, {
|
|
82
|
+
get when() {
|
|
83
|
+
return props.image;
|
|
84
|
+
},
|
|
85
|
+
get children() {
|
|
86
|
+
return createComponent(Button, {
|
|
87
|
+
type: "button",
|
|
88
|
+
variant: "ghost",
|
|
89
|
+
size: "sm",
|
|
90
|
+
get disabled() {
|
|
91
|
+
return props.disabled || busy();
|
|
92
|
+
},
|
|
93
|
+
onClick: () => {
|
|
94
|
+
setProblem(void 0);
|
|
95
|
+
props.onImageChange?.(void 0);
|
|
96
|
+
},
|
|
97
|
+
get children() {
|
|
98
|
+
return [createComponent(Trash2, { "aria-hidden": "true" }), "Remove image"];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}), null);
|
|
103
|
+
effect((_p$) => {
|
|
104
|
+
var _v$ = props.image ? "Replace image" : "Upload image", _v$2 = props.disabled;
|
|
105
|
+
_v$ !== _p$.e && setAttribute(_el$5, "aria-label", _p$.e = _v$);
|
|
106
|
+
_v$2 !== _p$.t && (_el$5.disabled = _p$.t = _v$2);
|
|
107
|
+
return _p$;
|
|
108
|
+
}, {
|
|
109
|
+
e: void 0,
|
|
110
|
+
t: void 0
|
|
111
|
+
});
|
|
112
|
+
return _el$2;
|
|
113
|
+
})(),
|
|
114
|
+
createComponent(Show, {
|
|
115
|
+
get when() {
|
|
116
|
+
return problem();
|
|
117
|
+
},
|
|
118
|
+
children: (message) => (() => {
|
|
119
|
+
var _el$1 = _tmpl$5();
|
|
120
|
+
insert(_el$1, message);
|
|
121
|
+
return _el$1;
|
|
122
|
+
})()
|
|
123
|
+
}),
|
|
124
|
+
(() => {
|
|
125
|
+
var _el$6 = _tmpl$2(), _el$9 = _el$6.firstChild.nextSibling;
|
|
126
|
+
_el$9.nextSibling;
|
|
127
|
+
insert(_el$6, MARK_IMAGE_MAX_SOURCE_BYTES / 1024 / 1024, _el$9);
|
|
128
|
+
return _el$6;
|
|
129
|
+
})()
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
}), null);
|
|
133
|
+
insert(_el$, createComponent(RadioGroup, {
|
|
134
|
+
get ["aria-label"]() {
|
|
135
|
+
return props.label;
|
|
136
|
+
},
|
|
137
|
+
get value() {
|
|
138
|
+
return memo(() => !!props.image)() ? "" : props.value ?? "";
|
|
139
|
+
},
|
|
140
|
+
get disabled() {
|
|
141
|
+
return props.disabled;
|
|
142
|
+
},
|
|
143
|
+
onChange: (key) => {
|
|
144
|
+
if (key) props.onValueChange(key);
|
|
145
|
+
},
|
|
146
|
+
"class": "grid grid-cols-[repeat(auto-fill,minmax(2.5rem,1fr))] gap-1.5",
|
|
147
|
+
get children() {
|
|
148
|
+
return createComponent(For, {
|
|
149
|
+
each: shellIconEntries,
|
|
150
|
+
children: (entry) => {
|
|
151
|
+
const Icon = resolveShellIcon(entry.key);
|
|
152
|
+
return createComponent(RadioGroup.Item, {
|
|
153
|
+
get value() {
|
|
154
|
+
return entry.key;
|
|
155
|
+
},
|
|
156
|
+
"class": "contents",
|
|
157
|
+
get children() {
|
|
158
|
+
return [createComponent(RadioGroup.ItemInput, { "class": "peer" }), createComponent(RadioGroup.ItemLabel, {
|
|
159
|
+
get title() {
|
|
160
|
+
return memo(() => entry.key === props.defaultKey)() ? `${entry.label} (default)` : entry.label;
|
|
161
|
+
},
|
|
162
|
+
get ["class"]() {
|
|
163
|
+
return cn("flex size-10 cursor-pointer items-center justify-center rounded-lg border border-border bg-card text-muted-foreground", "transition-colors ease-out", "hover:bg-surface-hover hover:text-foreground", "peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle", "data-[checked]:border-primary data-[checked]:bg-primary-subtle data-[checked]:text-primary", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50");
|
|
164
|
+
},
|
|
165
|
+
get children() {
|
|
166
|
+
return [createComponent(Icon, {
|
|
167
|
+
"class": "size-5",
|
|
168
|
+
"aria-hidden": "true"
|
|
169
|
+
}), (() => {
|
|
170
|
+
var _el$10 = _tmpl$6();
|
|
171
|
+
insert(_el$10, () => entry.label, null);
|
|
172
|
+
insert(_el$10, () => entry.key === props.defaultKey ? " (default)" : "", null);
|
|
173
|
+
return _el$10;
|
|
174
|
+
})()];
|
|
175
|
+
}
|
|
176
|
+
})];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}), null);
|
|
183
|
+
effect(() => className(_el$, cn("flex flex-col gap-3", props.class)));
|
|
184
|
+
return _el$;
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
function IconPickerDropdown(props) {
|
|
188
|
+
const [open, setOpen] = createSignal(false);
|
|
189
|
+
const [local, picker] = splitProps(props, ["trigger", "class"]);
|
|
190
|
+
const Current = () => resolveShellIcon(props.value ?? props.defaultKey ?? "");
|
|
191
|
+
return createComponent(Popover, {
|
|
192
|
+
get open() {
|
|
193
|
+
return open();
|
|
194
|
+
},
|
|
195
|
+
onOpenChange: setOpen,
|
|
196
|
+
placement: "bottom-start",
|
|
197
|
+
get children() {
|
|
198
|
+
return [createComponent(PopoverTrigger, {
|
|
199
|
+
as: Button,
|
|
200
|
+
type: "button",
|
|
201
|
+
variant: "outline",
|
|
202
|
+
"data-slot": "icon-picker-trigger",
|
|
203
|
+
get ["aria-label"]() {
|
|
204
|
+
return `${props.label}: ${props.image ? "uploaded image" : shellIconLabel(props.value ?? props.defaultKey ?? "")}`;
|
|
205
|
+
},
|
|
206
|
+
get disabled() {
|
|
207
|
+
return props.disabled;
|
|
208
|
+
},
|
|
209
|
+
get ["class"]() {
|
|
210
|
+
return cn("gap-2", local.class);
|
|
211
|
+
},
|
|
212
|
+
get children() {
|
|
213
|
+
return [(() => {
|
|
214
|
+
var _el$11 = _tmpl$7();
|
|
215
|
+
insert(_el$11, createComponent(Show, {
|
|
216
|
+
get when() {
|
|
217
|
+
return local.trigger;
|
|
218
|
+
},
|
|
219
|
+
get fallback() {
|
|
220
|
+
return createComponent(Show, {
|
|
221
|
+
get when() {
|
|
222
|
+
return props.image;
|
|
223
|
+
},
|
|
224
|
+
get fallback() {
|
|
225
|
+
return createComponent(Dynamic, {
|
|
226
|
+
get component() {
|
|
227
|
+
return Current();
|
|
228
|
+
},
|
|
229
|
+
"class": "size-4",
|
|
230
|
+
"aria-hidden": "true"
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
children: (src) => (() => {
|
|
234
|
+
var _el$12 = _tmpl$8();
|
|
235
|
+
effect(() => setAttribute(_el$12, "src", src()));
|
|
236
|
+
return _el$12;
|
|
237
|
+
})()
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
get children() {
|
|
241
|
+
return local.trigger;
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
return _el$11;
|
|
245
|
+
})(), createComponent(ChevronDown, {
|
|
246
|
+
"class": "size-4 text-muted-foreground",
|
|
247
|
+
"aria-hidden": "true"
|
|
248
|
+
})];
|
|
249
|
+
}
|
|
250
|
+
}), createComponent(PopoverContent, {
|
|
251
|
+
"class": "w-[min(22rem,var(--kb-popper-available-width))]",
|
|
252
|
+
get children() {
|
|
253
|
+
return createComponent(IconPicker, mergeProps(picker, { onValueChange: (key) => {
|
|
254
|
+
picker.onValueChange(key);
|
|
255
|
+
setOpen(false);
|
|
256
|
+
} }));
|
|
257
|
+
}
|
|
258
|
+
})];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
export { IconPicker, IconPickerDropdown };
|
|
264
|
+
|
|
265
|
+
//# sourceMappingURL=icon-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-picker.js","names":["RadioGroup","Radio","ChevronDown","ImageUp","Trash2","JSX","For","Show","createSignal","createUniqueId","splitProps","Dynamic","cn","Button","Popover","PopoverContent","PopoverTrigger","MarkImageError","markImageFromFile","markImageProblemCopy","MARK_IMAGE_MAX_SOURCE_BYTES","resolveShellIcon","shellIconEntries","shellIconLabel","IconPickerProps","label","value","onValueChange","key","defaultKey","image","onImageChange","disabled","class","IconPicker","props","inputId","problem","setProblem","busy","setBusy","fileInput","setFileInput","HTMLInputElement","upload","file","File","undefined","error","_el$","_tmpl$3","_$insert","_$createComponent","when","children","_el$2","_tmpl$","_el$3","firstChild","_el$4","nextSibling","_el$5","fallback","src","_el$0","_tmpl$4","_$effect","_$setAttribute","addEventListener","event","input","currentTarget","files","_$use","type","variant","size","onClick","click","_$memo","_p$","_v$","_v$2","e","t","message","_el$1","_tmpl$5","_el$6","_tmpl$2","_el$7","_el$9","_el$8","aria-label","onChange","each","entry","Icon","Item","ItemInput","ItemLabel","title","_el$10","_tmpl$6","_$className","IconPickerDropdownProps","trigger","Element","IconPickerDropdown","open","setOpen","local","picker","Current","onOpenChange","placement","as","_el$11","_tmpl$7","component","_el$12","_tmpl$8","_$mergeProps","_$createComponent","_$setAttribute","_$memo","Radio","_$className","_$mergeProps"],"sources":["../../../../src/components/composites/icon-picker/icon-picker.tsx"],"sourcesContent":["import { RadioGroup as Radio } from '@kobalte/core/radio-group'\nimport { ChevronDown, ImageUp, Trash2 } from 'lucide-solid'\nimport type { JSX } from 'solid-js'\nimport { For, Show, createSignal, createUniqueId, splitProps } from 'solid-js'\nimport { Dynamic } from 'solid-js/web'\nimport { cn } from '#lib/utils'\nimport { Button } from '../../ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'\nimport {\n MarkImageError,\n markImageFromFile,\n markImageProblemCopy,\n MARK_IMAGE_MAX_SOURCE_BYTES,\n} from './image-data-url'\nimport { resolveShellIcon, shellIconEntries, shellIconLabel } from './shell-icons'\n\n/**\n * IconPicker.\n *\n * The control behind \"choose an icon\" for a workspace or a project: a grid of\n * curated glyphs, and optionally an uploaded picture in their place.\n *\n * It is a single-choice group. The glyph grid is a Kobalte radio group, so\n * arrow keys move the choice and Tab enters and leaves the grid as one stop,\n * which is the contract a 56-cell grid needs to stay usable from a keyboard. An\n * uploaded image is a separate state, not a 57th cell: while `image` is set no\n * glyph is checked, and choosing a glyph asks the caller to drop the image, so\n * the stored mark is never both.\n *\n * The picker does not persist anything. The caller owns `value` and `image`\n * and saves them with the rest of its form; the image arrives already resized\n * to the stored square (see `markImageFromFile`).\n */\nexport type IconPickerProps = {\n /** The group's accessible name, e.g. \"Workspace icon\". */\n label: string\n /** The checked icon key; ignored while `image` is set. */\n value: string | undefined\n onValueChange(key: string): void\n /** The key the surface treats as unset, labelled \"default\" in the grid. */\n defaultKey?: string\n /** An uploaded mark (a data URL) currently chosen in place of a glyph. */\n image?: string\n /**\n * Called with the resized upload, or `undefined` for Remove. Omit it for a\n * surface that offers glyphs only; the upload row then does not render.\n */\n onImageChange?(image: string | undefined): void\n disabled?: boolean\n class?: string\n}\n\nexport function IconPicker(props: IconPickerProps) {\n const inputId = createUniqueId()\n const [problem, setProblem] = createSignal<string>()\n const [busy, setBusy] = createSignal(false)\n const [fileInput, setFileInput] = createSignal<HTMLInputElement>()\n\n const upload = async (file: File | undefined) => {\n if (!file || !props.onImageChange) return\n setProblem(undefined)\n setBusy(true)\n try {\n props.onImageChange(await markImageFromFile(file))\n } catch (error) {\n setProblem(\n markImageProblemCopy[error instanceof MarkImageError ? error.problem : 'unreadable']\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <div data-slot=\"icon-picker\" class={cn('flex flex-col gap-3', props.class)}>\n <Show when={props.onImageChange}>\n <div class=\"flex items-center gap-3\" data-slot=\"icon-picker-upload\">\n <span\n aria-hidden=\"true\"\n class=\"flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-muted\"\n >\n <Show\n when={props.image}\n fallback={<ImageUp class=\"size-5 text-muted-foreground\" aria-hidden=\"true\" />}\n >\n {(src) => <img src={src()} alt=\"\" class=\"size-full object-cover\" />}\n </Show>\n </span>\n <div class=\"flex flex-wrap items-center gap-2\">\n <input\n ref={setFileInput}\n id={inputId}\n type=\"file\"\n class=\"sr-only\"\n tabIndex={-1}\n // The visible button drives this input; hide the duplicate from assistive\n // technology but keep a name so the form-label rule is satisfied.\n aria-hidden=\"true\"\n aria-label={props.image ? 'Replace image' : 'Upload image'}\n accept=\"image/png,image/jpeg,image/webp,image/gif\"\n disabled={props.disabled}\n onChange={(event) => {\n const input = event.currentTarget\n void upload(input.files?.[0])\n // Clearing the value lets the same file be chosen again after Remove.\n input.value = ''\n }}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={props.disabled || busy()}\n onClick={() => fileInput()?.click()}\n >\n <ImageUp aria-hidden=\"true\" />\n {props.image ? 'Replace image' : 'Upload image'}\n </Button>\n <Show when={props.image}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={props.disabled || busy()}\n onClick={() => {\n setProblem(undefined)\n props.onImageChange?.(undefined)\n }}\n >\n <Trash2 aria-hidden=\"true\" />\n Remove image\n </Button>\n </Show>\n </div>\n </div>\n <Show when={problem()}>\n {(message) => (\n <p role=\"alert\" class=\"text-sm text-destructive\">\n {message()}\n </p>\n )}\n </Show>\n <p class=\"text-xs text-muted-foreground\">\n PNG, JPEG, WebP or GIF up to {MARK_IMAGE_MAX_SOURCE_BYTES / 1024 / 1024} MB. It is cropped\n to a square.\n </p>\n </Show>\n <Radio\n aria-label={props.label}\n value={props.image ? '' : (props.value ?? '')}\n disabled={props.disabled}\n onChange={(key) => {\n if (key) props.onValueChange(key)\n }}\n class=\"grid grid-cols-[repeat(auto-fill,minmax(2.5rem,1fr))] gap-1.5\"\n >\n <For each={shellIconEntries}>\n {(entry) => {\n const Icon = resolveShellIcon(entry.key)\n return (\n <Radio.Item value={entry.key} class=\"contents\">\n <Radio.ItemInput class=\"peer\" />\n <Radio.ItemLabel\n title={entry.key === props.defaultKey ? `${entry.label} (default)` : entry.label}\n class={cn(\n 'flex size-10 cursor-pointer items-center justify-center rounded-lg border border-border bg-card text-muted-foreground',\n 'transition-colors ease-out',\n 'hover:bg-surface-hover hover:text-foreground',\n 'peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle',\n 'data-[checked]:border-primary data-[checked]:bg-primary-subtle data-[checked]:text-primary',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50'\n )}\n >\n <Icon class=\"size-5\" aria-hidden=\"true\" />\n <span class=\"sr-only\">\n {entry.label}\n {entry.key === props.defaultKey ? ' (default)' : ''}\n </span>\n </Radio.ItemLabel>\n </Radio.Item>\n )\n }}\n </For>\n </Radio>\n </div>\n )\n}\n\n/**\n * IconPickerDropdown.\n *\n * The compact form of the picker for a settings row: one button showing the\n * current mark, opening the full grid (and the upload row, when the host offers\n * it) in a popover. Choosing a glyph applies it and closes the popover; an\n * upload or Remove leaves it open so the result is visible. The host owns the\n * value exactly as it does for `IconPicker`.\n */\nexport type IconPickerDropdownProps = IconPickerProps & {\n /** The mark shown on the trigger; defaults to the checked glyph or the uploaded image. */\n trigger?: JSX.Element\n}\n\nexport function IconPickerDropdown(props: IconPickerDropdownProps) {\n const [open, setOpen] = createSignal(false)\n const [local, picker] = splitProps(props, ['trigger', 'class'])\n const Current = () => resolveShellIcon(props.value ?? props.defaultKey ?? '')\n return (\n <Popover open={open()} onOpenChange={setOpen} placement=\"bottom-start\">\n <PopoverTrigger\n as={Button}\n type=\"button\"\n variant=\"outline\"\n data-slot=\"icon-picker-trigger\"\n aria-label={`${props.label}: ${props.image ? 'uploaded image' : shellIconLabel(props.value ?? props.defaultKey ?? '')}`}\n disabled={props.disabled}\n class={cn('gap-2', local.class)}\n >\n <span aria-hidden=\"true\" class=\"flex size-5 items-center justify-center overflow-hidden\">\n <Show\n when={local.trigger}\n fallback={\n <Show\n when={props.image}\n fallback={<Dynamic component={Current()} class=\"size-4\" aria-hidden=\"true\" />}\n >\n {(src) => <img src={src()} alt=\"\" class=\"size-full rounded-sm object-cover\" />}\n </Show>\n }\n >\n {local.trigger}\n </Show>\n </span>\n <ChevronDown class=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </PopoverTrigger>\n <PopoverContent class=\"w-[min(22rem,var(--kb-popper-available-width))]\">\n <IconPicker\n {...picker}\n onValueChange={(key) => {\n picker.onValueChange(key)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an uploaded picture into the small square mark a workspace stores.
|
|
3
|
+
*
|
|
4
|
+
* The mark travels with the workspace record (it is shown in every list the
|
|
5
|
+
* workspace appears in), so it is resized to a fixed square and re-encoded
|
|
6
|
+
* before it leaves the browser rather than stored as the user's original file.
|
|
7
|
+
* Re-encoding also drops metadata and any embedded script an SVG could carry:
|
|
8
|
+
* only raster images are accepted and the result is always a `data:image/webp`
|
|
9
|
+
* or `data:image/png` URL.
|
|
10
|
+
*/
|
|
11
|
+
/** The edge, in pixels, of the stored square. 2x the largest tile the apps draw. */
|
|
12
|
+
export declare const MARK_IMAGE_SIZE = 128;
|
|
13
|
+
/** The largest source file read, before resizing. */
|
|
14
|
+
export declare const MARK_IMAGE_MAX_SOURCE_BYTES: number;
|
|
15
|
+
/** The largest encoded result kept; a stored mark never exceeds this many characters. */
|
|
16
|
+
export declare const MARK_IMAGE_MAX_DATA_URL_LENGTH: number;
|
|
17
|
+
export type MarkImageProblem = 'type' | 'size' | 'unreadable' | 'too-large';
|
|
18
|
+
/** Thrown by {@link markImageFromFile}; `problem` keys {@link markImageProblemCopy}. */
|
|
19
|
+
export declare class MarkImageError extends Error {
|
|
20
|
+
readonly problem: MarkImageProblem;
|
|
21
|
+
constructor(problem: MarkImageProblem);
|
|
22
|
+
}
|
|
23
|
+
export declare const markImageProblemCopy: Readonly<Record<MarkImageProblem, string>>;
|
|
24
|
+
/** Whether a stored value is a mark image this module could have produced. */
|
|
25
|
+
export declare function isMarkImageDataUrl(value: unknown): value is string;
|
|
26
|
+
/** Pre-flight checks that need no decoding; `undefined` means the file may be read. */
|
|
27
|
+
export declare function checkMarkImageFile(file: Readonly<{
|
|
28
|
+
type: string;
|
|
29
|
+
size: number;
|
|
30
|
+
}>): MarkImageProblem | undefined;
|
|
31
|
+
/** The source rectangle of the largest centred square, so the mark is a cover crop. */
|
|
32
|
+
export declare function centredSquare(width: number, height: number): Readonly<{
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
side: number;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Resize an image file to the stored mark. Rejects with a {@link MarkImageError}
|
|
39
|
+
* so the caller can show `markImageProblemCopy[error.problem]`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function markImageFromFile(file: File): Promise<string>;
|
|
42
|
+
//# sourceMappingURL=image-data-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-data-url.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/icon-picker/image-data-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,oFAAoF;AACpF,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,qDAAqD;AACrD,eAAO,MAAM,2BAA2B,QAAkB,CAAA;AAC1D,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,QAAY,CAAA;AAIvD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,CAAA;AAE3E,wFAAwF;AACxF,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,YAAY,OAAO,EAAE,gBAAgB,EAIpC;CACF;AAED,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAK3E,CAAA;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAMlE;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,gBAAgB,GAAG,SAAS,CAI9B;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,QAAQ,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAGlD;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCnE"}
|