@sesionario/blocks 1.3.1 → 1.4.1

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/README.md CHANGED
@@ -12,6 +12,14 @@ Shared feedback patterns include `ConfirmDestructiveDialog` (confirm before dele
12
12
  pnpm add @sesionario/blocks
13
13
  ```
14
14
 
15
+ The main entry is a Client Component boundary. Pure helpers that must run in
16
+ React Server Components (e.g. avatar initials/colors) should be imported from
17
+ the server-safe subpath:
18
+
19
+ ```ts
20
+ import { avatarFallbackStyle, getInitials } from "@sesionario/blocks/avatar";
21
+ ```
22
+
15
23
  Peer dependencies (install in the consuming app as needed):
16
24
 
17
25
  - `react`, `react-dom`
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ // src/lib/avatar.ts
4
+ var EMPTY_COLORS = {
5
+ backgroundColor: "#9ca3af",
6
+ color: "#111111"
7
+ };
8
+ var AVATAR_PALETTE = [
9
+ "#4f6d8c",
10
+ // slate blue
11
+ "#3d7a6a",
12
+ // teal
13
+ "#6b5b8a",
14
+ // muted purple
15
+ "#8a5a4a",
16
+ // warm brown
17
+ "#4a7a8a",
18
+ // steel cyan
19
+ "#7a6b4a",
20
+ // olive
21
+ "#5a6b8a",
22
+ // periwinkle
23
+ "#8a4a5a",
24
+ // rose
25
+ "#4a8a6b",
26
+ // green
27
+ "#6b4a7a",
28
+ // plum
29
+ "#8a6b4a",
30
+ // amber brown
31
+ "#4a5a8a",
32
+ // indigo
33
+ "#7a4a6b",
34
+ // mauve
35
+ "#5a8a4a",
36
+ // leaf
37
+ "#8a5a7a",
38
+ // dusty pink
39
+ "#3d6b7a"
40
+ // deep teal
41
+ ];
42
+ function hashString(input) {
43
+ let hash = 2166136261;
44
+ for (let i = 0; i < input.length; i++) {
45
+ hash ^= input.charCodeAt(i);
46
+ hash = Math.imul(hash, 16777619);
47
+ }
48
+ return hash >>> 0;
49
+ }
50
+ function srgbToLinear(channel) {
51
+ const c = channel / 255;
52
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
53
+ }
54
+ function relativeLuminance(hex) {
55
+ const normalized = hex.replace("#", "");
56
+ const r = Number.parseInt(normalized.slice(0, 2), 16);
57
+ const g = Number.parseInt(normalized.slice(2, 4), 16);
58
+ const b = Number.parseInt(normalized.slice(4, 6), 16);
59
+ return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
60
+ }
61
+ function contrastingTextColor(backgroundHex) {
62
+ return relativeLuminance(backgroundHex) > 0.179 ? "#111111" : "#ffffff";
63
+ }
64
+ function getInitials(name, max = 2) {
65
+ const parts = name.trim().split(/\s+/).filter(Boolean);
66
+ if (parts.length === 0) return "?";
67
+ const letters = parts.slice(0, Math.max(1, max)).map((part) => part.charAt(0)).join("");
68
+ return letters.toUpperCase() || "?";
69
+ }
70
+ function getAvatarColors(name) {
71
+ const normalized = name.trim().toLowerCase();
72
+ if (!normalized) {
73
+ return { ...EMPTY_COLORS };
74
+ }
75
+ const backgroundColor = AVATAR_PALETTE[hashString(normalized) % AVATAR_PALETTE.length];
76
+ return {
77
+ backgroundColor,
78
+ color: contrastingTextColor(backgroundColor)
79
+ };
80
+ }
81
+ function avatarFallbackStyle(name) {
82
+ return getAvatarColors(name);
83
+ }
84
+
85
+ exports.avatarFallbackStyle = avatarFallbackStyle;
86
+ exports.getAvatarColors = getAvatarColors;
87
+ exports.getInitials = getInitials;
88
+ //# sourceMappingURL=avatar.cjs.map
89
+ //# sourceMappingURL=avatar.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/avatar.ts"],"names":[],"mappings":"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","file":"avatar.cjs","sourcesContent":["/** Neutral fallback when the name is empty. */\nconst EMPTY_COLORS = {\n\tbackgroundColor: \"#9ca3af\",\n\tcolor: \"#111111\",\n} as const;\n\n/**\n * Curated mid-lightness palette — professional, calm hues that stay\n * distinct when hashed by name and contrast well with black or white text.\n */\nconst AVATAR_PALETTE = [\n\t\"#4f6d8c\", // slate blue\n\t\"#3d7a6a\", // teal\n\t\"#6b5b8a\", // muted purple\n\t\"#8a5a4a\", // warm brown\n\t\"#4a7a8a\", // steel cyan\n\t\"#7a6b4a\", // olive\n\t\"#5a6b8a\", // periwinkle\n\t\"#8a4a5a\", // rose\n\t\"#4a8a6b\", // green\n\t\"#6b4a7a\", // plum\n\t\"#8a6b4a\", // amber brown\n\t\"#4a5a8a\", // indigo\n\t\"#7a4a6b\", // mauve\n\t\"#5a8a4a\", // leaf\n\t\"#8a5a7a\", // dusty pink\n\t\"#3d6b7a\", // deep teal\n] as const;\n\nexport type AvatarColors = {\n\tbackgroundColor: string;\n\tcolor: string;\n};\n\n/** FNV-1a 32-bit hash — stable across runtimes for the same string. */\nfunction hashString(input: string): number {\n\tlet hash = 0x811c9dc5;\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash ^= input.charCodeAt(i);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn hash >>> 0;\n}\n\nfunction srgbToLinear(channel: number): number {\n\tconst c = channel / 255;\n\treturn c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n}\n\n/** Relative luminance (WCAG) for a hex color like `#rrggbb`. */\nfunction relativeLuminance(hex: string): number {\n\tconst normalized = hex.replace(\"#\", \"\");\n\tconst r = Number.parseInt(normalized.slice(0, 2), 16);\n\tconst g = Number.parseInt(normalized.slice(2, 4), 16);\n\tconst b = Number.parseInt(normalized.slice(4, 6), 16);\n\treturn (\n\t\t0.2126 * srgbToLinear(r) +\n\t\t0.7152 * srgbToLinear(g) +\n\t\t0.0722 * srgbToLinear(b)\n\t);\n}\n\nfunction contrastingTextColor(backgroundHex: string): \"#ffffff\" | \"#111111\" {\n\t// Threshold ~0.179 ≈ contrast preference for mid tones (WCAG-inspired).\n\treturn relativeLuminance(backgroundHex) > 0.179 ? \"#111111\" : \"#ffffff\";\n}\n\n/**\n * Word-based initials from a display name (max letters, default 2).\n * Empty / whitespace-only names yield `\"?\"`.\n */\nexport function getInitials(name: string, max = 2): string {\n\tconst parts = name.trim().split(/\\s+/).filter(Boolean);\n\tif (parts.length === 0) return \"?\";\n\tconst letters = parts\n\t\t.slice(0, Math.max(1, max))\n\t\t.map((part) => part.charAt(0))\n\t\t.join(\"\");\n\treturn letters.toUpperCase() || \"?\";\n}\n\n/**\n * Deterministic background + contrasting foreground from a full name.\n * Same name (case-insensitive) always maps to the same colors.\n */\nexport function getAvatarColors(name: string): AvatarColors {\n\tconst normalized = name.trim().toLowerCase();\n\tif (!normalized) {\n\t\treturn { ...EMPTY_COLORS };\n\t}\n\tconst backgroundColor =\n\t\tAVATAR_PALETTE[hashString(normalized) % AVATAR_PALETTE.length]!;\n\treturn {\n\t\tbackgroundColor,\n\t\tcolor: contrastingTextColor(backgroundColor),\n\t};\n}\n\n/** CSS style object for `AvatarFallback` (or any letter-avatar container). */\nexport function avatarFallbackStyle(\n\tname: string,\n): Pick<AvatarColors, \"backgroundColor\" | \"color\"> {\n\treturn getAvatarColors(name);\n}\n"]}
@@ -0,0 +1,18 @@
1
+ type AvatarColors = {
2
+ backgroundColor: string;
3
+ color: string;
4
+ };
5
+ /**
6
+ * Word-based initials from a display name (max letters, default 2).
7
+ * Empty / whitespace-only names yield `"?"`.
8
+ */
9
+ declare function getInitials(name: string, max?: number): string;
10
+ /**
11
+ * Deterministic background + contrasting foreground from a full name.
12
+ * Same name (case-insensitive) always maps to the same colors.
13
+ */
14
+ declare function getAvatarColors(name: string): AvatarColors;
15
+ /** CSS style object for `AvatarFallback` (or any letter-avatar container). */
16
+ declare function avatarFallbackStyle(name: string): Pick<AvatarColors, "backgroundColor" | "color">;
17
+
18
+ export { type AvatarColors, avatarFallbackStyle, getAvatarColors, getInitials };
@@ -0,0 +1,18 @@
1
+ type AvatarColors = {
2
+ backgroundColor: string;
3
+ color: string;
4
+ };
5
+ /**
6
+ * Word-based initials from a display name (max letters, default 2).
7
+ * Empty / whitespace-only names yield `"?"`.
8
+ */
9
+ declare function getInitials(name: string, max?: number): string;
10
+ /**
11
+ * Deterministic background + contrasting foreground from a full name.
12
+ * Same name (case-insensitive) always maps to the same colors.
13
+ */
14
+ declare function getAvatarColors(name: string): AvatarColors;
15
+ /** CSS style object for `AvatarFallback` (or any letter-avatar container). */
16
+ declare function avatarFallbackStyle(name: string): Pick<AvatarColors, "backgroundColor" | "color">;
17
+
18
+ export { type AvatarColors, avatarFallbackStyle, getAvatarColors, getInitials };
package/dist/avatar.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/lib/avatar.ts
2
+ var EMPTY_COLORS = {
3
+ backgroundColor: "#9ca3af",
4
+ color: "#111111"
5
+ };
6
+ var AVATAR_PALETTE = [
7
+ "#4f6d8c",
8
+ // slate blue
9
+ "#3d7a6a",
10
+ // teal
11
+ "#6b5b8a",
12
+ // muted purple
13
+ "#8a5a4a",
14
+ // warm brown
15
+ "#4a7a8a",
16
+ // steel cyan
17
+ "#7a6b4a",
18
+ // olive
19
+ "#5a6b8a",
20
+ // periwinkle
21
+ "#8a4a5a",
22
+ // rose
23
+ "#4a8a6b",
24
+ // green
25
+ "#6b4a7a",
26
+ // plum
27
+ "#8a6b4a",
28
+ // amber brown
29
+ "#4a5a8a",
30
+ // indigo
31
+ "#7a4a6b",
32
+ // mauve
33
+ "#5a8a4a",
34
+ // leaf
35
+ "#8a5a7a",
36
+ // dusty pink
37
+ "#3d6b7a"
38
+ // deep teal
39
+ ];
40
+ function hashString(input) {
41
+ let hash = 2166136261;
42
+ for (let i = 0; i < input.length; i++) {
43
+ hash ^= input.charCodeAt(i);
44
+ hash = Math.imul(hash, 16777619);
45
+ }
46
+ return hash >>> 0;
47
+ }
48
+ function srgbToLinear(channel) {
49
+ const c = channel / 255;
50
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
51
+ }
52
+ function relativeLuminance(hex) {
53
+ const normalized = hex.replace("#", "");
54
+ const r = Number.parseInt(normalized.slice(0, 2), 16);
55
+ const g = Number.parseInt(normalized.slice(2, 4), 16);
56
+ const b = Number.parseInt(normalized.slice(4, 6), 16);
57
+ return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
58
+ }
59
+ function contrastingTextColor(backgroundHex) {
60
+ return relativeLuminance(backgroundHex) > 0.179 ? "#111111" : "#ffffff";
61
+ }
62
+ function getInitials(name, max = 2) {
63
+ const parts = name.trim().split(/\s+/).filter(Boolean);
64
+ if (parts.length === 0) return "?";
65
+ const letters = parts.slice(0, Math.max(1, max)).map((part) => part.charAt(0)).join("");
66
+ return letters.toUpperCase() || "?";
67
+ }
68
+ function getAvatarColors(name) {
69
+ const normalized = name.trim().toLowerCase();
70
+ if (!normalized) {
71
+ return { ...EMPTY_COLORS };
72
+ }
73
+ const backgroundColor = AVATAR_PALETTE[hashString(normalized) % AVATAR_PALETTE.length];
74
+ return {
75
+ backgroundColor,
76
+ color: contrastingTextColor(backgroundColor)
77
+ };
78
+ }
79
+ function avatarFallbackStyle(name) {
80
+ return getAvatarColors(name);
81
+ }
82
+
83
+ export { avatarFallbackStyle, getAvatarColors, getInitials };
84
+ //# sourceMappingURL=avatar.js.map
85
+ //# sourceMappingURL=avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/avatar.ts"],"names":[],"mappings":";AACA,IAAM,YAAA,GAAe;AAAA,EACpB,eAAA,EAAiB,SAAA;AAAA,EACjB,KAAA,EAAO;AACR,CAAA;AAMA,IAAM,cAAA,GAAiB;AAAA,EACtB,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA,SAAA;AAAA;AAAA,EACA;AAAA;AACD,CAAA;AAQA,SAAS,WAAW,KAAA,EAAuB;AAC1C,EAAA,IAAI,IAAA,GAAO,UAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACtC,IAAA,IAAA,IAAQ,KAAA,CAAM,WAAW,CAAC,CAAA;AAC1B,IAAA,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,QAAU,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,IAAA,KAAS,CAAA;AACjB;AAEA,SAAS,aAAa,OAAA,EAAyB;AAC9C,EAAA,MAAM,IAAI,OAAA,GAAU,GAAA;AACpB,EAAA,OAAO,KAAK,OAAA,GAAU,CAAA,GAAI,KAAA,GAAA,CAAA,CAAU,CAAA,GAAI,SAAS,KAAA,KAAU,GAAA;AAC5D;AAGA,SAAS,kBAAkB,GAAA,EAAqB;AAC/C,EAAA,MAAM,UAAA,GAAa,GAAA,CAAI,OAAA,CAAQ,GAAA,EAAK,EAAE,CAAA;AACtC,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,UAAA,CAAW,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACpD,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,UAAA,CAAW,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACpD,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,UAAA,CAAW,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AACpD,EAAA,OACC,MAAA,GAAS,YAAA,CAAa,CAAC,CAAA,GACvB,MAAA,GAAS,aAAa,CAAC,CAAA,GACvB,MAAA,GAAS,YAAA,CAAa,CAAC,CAAA;AAEzB;AAEA,SAAS,qBAAqB,aAAA,EAA8C;AAE3E,EAAA,OAAO,iBAAA,CAAkB,aAAa,CAAA,GAAI,KAAA,GAAQ,SAAA,GAAY,SAAA;AAC/D;AAMO,SAAS,WAAA,CAAY,IAAA,EAAc,GAAA,GAAM,CAAA,EAAW;AAC1D,EAAA,MAAM,KAAA,GAAQ,KAAK,IAAA,EAAK,CAAE,MAAM,KAAK,CAAA,CAAE,OAAO,OAAO,CAAA;AACrD,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,GAAA;AAC/B,EAAA,MAAM,UAAU,KAAA,CACd,KAAA,CAAM,GAAG,IAAA,CAAK,GAAA,CAAI,GAAG,GAAG,CAAC,EACzB,GAAA,CAAI,CAAC,SAAS,IAAA,CAAK,MAAA,CAAO,CAAC,CAAC,CAAA,CAC5B,KAAK,EAAE,CAAA;AACT,EAAA,OAAO,OAAA,CAAQ,aAAY,IAAK,GAAA;AACjC;AAMO,SAAS,gBAAgB,IAAA,EAA4B;AAC3D,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY;AAC3C,EAAA,IAAI,CAAC,UAAA,EAAY;AAChB,IAAA,OAAO,EAAE,GAAG,YAAA,EAAa;AAAA,EAC1B;AACA,EAAA,MAAM,kBACL,cAAA,CAAe,UAAA,CAAW,UAAU,CAAA,GAAI,eAAe,MAAM,CAAA;AAC9D,EAAA,OAAO;AAAA,IACN,eAAA;AAAA,IACA,KAAA,EAAO,qBAAqB,eAAe;AAAA,GAC5C;AACD;AAGO,SAAS,oBACf,IAAA,EACkD;AAClD,EAAA,OAAO,gBAAgB,IAAI,CAAA;AAC5B","file":"avatar.js","sourcesContent":["/** Neutral fallback when the name is empty. */\nconst EMPTY_COLORS = {\n\tbackgroundColor: \"#9ca3af\",\n\tcolor: \"#111111\",\n} as const;\n\n/**\n * Curated mid-lightness palette — professional, calm hues that stay\n * distinct when hashed by name and contrast well with black or white text.\n */\nconst AVATAR_PALETTE = [\n\t\"#4f6d8c\", // slate blue\n\t\"#3d7a6a\", // teal\n\t\"#6b5b8a\", // muted purple\n\t\"#8a5a4a\", // warm brown\n\t\"#4a7a8a\", // steel cyan\n\t\"#7a6b4a\", // olive\n\t\"#5a6b8a\", // periwinkle\n\t\"#8a4a5a\", // rose\n\t\"#4a8a6b\", // green\n\t\"#6b4a7a\", // plum\n\t\"#8a6b4a\", // amber brown\n\t\"#4a5a8a\", // indigo\n\t\"#7a4a6b\", // mauve\n\t\"#5a8a4a\", // leaf\n\t\"#8a5a7a\", // dusty pink\n\t\"#3d6b7a\", // deep teal\n] as const;\n\nexport type AvatarColors = {\n\tbackgroundColor: string;\n\tcolor: string;\n};\n\n/** FNV-1a 32-bit hash — stable across runtimes for the same string. */\nfunction hashString(input: string): number {\n\tlet hash = 0x811c9dc5;\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash ^= input.charCodeAt(i);\n\t\thash = Math.imul(hash, 0x01000193);\n\t}\n\treturn hash >>> 0;\n}\n\nfunction srgbToLinear(channel: number): number {\n\tconst c = channel / 255;\n\treturn c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n}\n\n/** Relative luminance (WCAG) for a hex color like `#rrggbb`. */\nfunction relativeLuminance(hex: string): number {\n\tconst normalized = hex.replace(\"#\", \"\");\n\tconst r = Number.parseInt(normalized.slice(0, 2), 16);\n\tconst g = Number.parseInt(normalized.slice(2, 4), 16);\n\tconst b = Number.parseInt(normalized.slice(4, 6), 16);\n\treturn (\n\t\t0.2126 * srgbToLinear(r) +\n\t\t0.7152 * srgbToLinear(g) +\n\t\t0.0722 * srgbToLinear(b)\n\t);\n}\n\nfunction contrastingTextColor(backgroundHex: string): \"#ffffff\" | \"#111111\" {\n\t// Threshold ~0.179 ≈ contrast preference for mid tones (WCAG-inspired).\n\treturn relativeLuminance(backgroundHex) > 0.179 ? \"#111111\" : \"#ffffff\";\n}\n\n/**\n * Word-based initials from a display name (max letters, default 2).\n * Empty / whitespace-only names yield `\"?\"`.\n */\nexport function getInitials(name: string, max = 2): string {\n\tconst parts = name.trim().split(/\\s+/).filter(Boolean);\n\tif (parts.length === 0) return \"?\";\n\tconst letters = parts\n\t\t.slice(0, Math.max(1, max))\n\t\t.map((part) => part.charAt(0))\n\t\t.join(\"\");\n\treturn letters.toUpperCase() || \"?\";\n}\n\n/**\n * Deterministic background + contrasting foreground from a full name.\n * Same name (case-insensitive) always maps to the same colors.\n */\nexport function getAvatarColors(name: string): AvatarColors {\n\tconst normalized = name.trim().toLowerCase();\n\tif (!normalized) {\n\t\treturn { ...EMPTY_COLORS };\n\t}\n\tconst backgroundColor =\n\t\tAVATAR_PALETTE[hashString(normalized) % AVATAR_PALETTE.length]!;\n\treturn {\n\t\tbackgroundColor,\n\t\tcolor: contrastingTextColor(backgroundColor),\n\t};\n}\n\n/** CSS style object for `AvatarFallback` (or any letter-avatar container). */\nexport function avatarFallbackStyle(\n\tname: string,\n): Pick<AvatarColors, \"backgroundColor\" | \"color\"> {\n\treturn getAvatarColors(name);\n}\n"]}