ab-nextjs-components 0.1.9 โ†’ 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/ab-brand/index.tsx +30 -1
  3. package/ab-brand/styles.module.css +40 -1
  4. package/ab-button/helpers.ts +90 -0
  5. package/ab-button/index.tsx +164 -87
  6. package/ab-button/styles.module.css +79 -1
  7. package/ab-carousel/helpers.ts +146 -0
  8. package/ab-carousel/index.tsx +643 -0
  9. package/ab-carousel/styles.module.css +206 -0
  10. package/ab-chip/index.tsx +75 -0
  11. package/ab-chip/styles.module.css +87 -0
  12. package/ab-collapsible/index.tsx +30 -1
  13. package/ab-collapsible/styles.module.css +40 -1
  14. package/ab-flow-diagram/helpers.ts +77 -0
  15. package/ab-flow-diagram/index.tsx +152 -0
  16. package/ab-flow-diagram/styles.module.css +158 -0
  17. package/ab-icon/builtin.ts +61 -0
  18. package/ab-icon/helpers.ts +102 -0
  19. package/ab-icon/index.tsx +77 -16
  20. package/ab-icon/styles.module.css +53 -9
  21. package/ab-icon-button/index.tsx +110 -46
  22. package/ab-icon-button/styles.module.css +28 -10
  23. package/ab-install-snippet/helpers.ts +62 -0
  24. package/ab-install-snippet/index.tsx +170 -0
  25. package/ab-install-snippet/styles.module.css +104 -0
  26. package/ab-like-button/index.tsx +33 -1
  27. package/ab-like-button/styles.module.css +41 -6
  28. package/ab-linear-progress/index.tsx +5 -1
  29. package/ab-linear-progress/styles.module.css +8 -24
  30. package/ab-locale-switcher/index.tsx +213 -0
  31. package/ab-locale-switcher/styles.module.css +127 -0
  32. package/ab-pill/index.tsx +74 -0
  33. package/ab-progress-chips/index.tsx +30 -1
  34. package/ab-progress-chips/styles.module.css +40 -1
  35. package/ab-searchbar/index.tsx +33 -2
  36. package/ab-searchbar/styles.module.css +41 -5
  37. package/ab-splash/index.tsx +3 -1
  38. package/ab-splash/styles.module.css +2 -5
  39. package/ab-theme-toggle/index.tsx +188 -0
  40. package/ab-theme-toggle/styles.module.css +89 -0
  41. package/css/dialog.css +38 -3
  42. package/index.ts +41 -0
  43. package/package.json +33 -17
  44. package/server/ab-brand/index.tsx +29 -1
  45. package/server/ab-brand/styles.module.css +40 -1
  46. package/server/ab-button/index.tsx +96 -79
  47. package/server/ab-button/styles.module.css +79 -1
  48. package/server/ab-icon/index.tsx +68 -16
  49. package/server/ab-icon/styles.module.css +53 -9
@@ -4,7 +4,30 @@
4
4
  * ab-nextjs-components
5
5
  * ~~~~~~~~~~~~
6
6
  * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
- * Plain CSS modules โ€” no Tailwind Tailwind apply (Turbopack-safe).
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: AbIcon - Styles (G2)
28
+ * @file: ab-icon/styles.module.css
29
+ * @type: CSS module
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
8
31
  */
9
32
 
10
33
  /*
@@ -13,15 +36,36 @@
13
36
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14
37
  */
15
38
 
39
+ /* ๐Ÿ”ฃ SVG icon box */
16
40
  .abIcon {
17
- display: inline-flex;
18
- align-items: center;
19
- justify-content: center;
20
- font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
21
- line-height: 1;
22
- user-select: none;
41
+ display: inline-flex; /* ๐Ÿ“ */
42
+ flex: none; /* ๐Ÿšซ squash */
43
+ width: var(--ab-icon-size, 24px); /* ๐Ÿ“ */
44
+ height: var(--ab-icon-size, 24px); /* ๐Ÿ“ */
45
+ line-height: 0; /* ๐Ÿงน no baseline gap */
46
+ color: inherit; /* ๐ŸŽจ currentColor flows into the SVG */
23
47
  }
24
48
 
25
- .abIcon[data-filled='true'] {
26
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
49
+ /* ๐Ÿ“ the inline SVG fills the box */
50
+ .abIcon > svg {
51
+ width: 100%; /* โ†”๏ธ */
52
+ height: 100%; /* โ†•๏ธ */
53
+ }
54
+
55
+ /* ๐Ÿ“ weight: CSS beats SVG presentation attributes, outlined only (filled stay solid) */
56
+ .abIcon[data-variant='outlined'] > svg [stroke] {
57
+ stroke-width: var(--ab-icon-stroke, 1.25); /* โœ๏ธ */
58
+ }
59
+
60
+ /* ๐Ÿ•ฐ๏ธ Material Symbols fallback (unchanged from pre-G2) */
61
+ .abIconFont {
62
+ display: inline-flex; /* ๐Ÿ“ */
63
+ align-items: center; /* โ†•๏ธ */
64
+ justify-content: center; /* โ†”๏ธ */
65
+ font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; /* ๐Ÿ”ค */
66
+ line-height: 1; /* ๐Ÿ“ */
67
+ user-select: none; /* ๐Ÿšซ */
68
+ }
69
+ .abIconFont[data-filled='true'] {
70
+ font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; /* โฌ› */
27
71
  }
@@ -5,103 +5,167 @@
5
5
  * ~~~~~~~~~~~~
6
6
  * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
7
  *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
8
26
  * @project: ab-nextjs-components
9
27
  * @name: AbIconButton - Client
10
28
  * @file: ab-icon-button/index.tsx
29
+ * @type: Component (client) โ€” G6: accessible name on the interactive element
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ *
32
+ * Example usage:
33
+ * 1+|> <AbIconButton icon="arrow_back" label={t('back')} onClick={prev} />
34
+ * -|> <AbIconButton icon="open_in_new" href="/docs" label="Open docs" /> // ๐Ÿ”— link variant is named too
35
+ */
36
+
37
+ /*
38
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
39
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
40
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11
41
  */
12
42
 
13
43
  'use client';
14
44
 
45
+ // โš›๏ธ React types + forwardRef
15
46
  import type { CSSProperties, MouseEvent } from 'react';
16
47
  import { forwardRef } from 'react';
48
+ // ๐Ÿงต class-name joiner
17
49
  import clsx from 'clsx';
50
+ // ๐ŸŽจ package-shipped CSS module
18
51
  import styles from './styles.module.css';
52
+ import { AB_MOTION } from 'ab-nextjs-animations/motion'; // ๐ŸŽฌ transition class names (ab-nextjs-animations >= 0.2.4)
53
+ import 'ab-nextjs-animations/motion/styles.css'; // ๐ŸŽจ .ab-motion-*
19
54
 
55
+ // โš™๏ธ public props
20
56
  export interface AbIconButtonProps {
57
+ // ๐Ÿท๏ธ extra classes
21
58
  className?: string;
59
+ // ๐ŸŽจ inline style
22
60
  style?: CSSProperties;
61
+ // ๐Ÿ™ˆ hide the whole button
23
62
  hidden?: boolean;
63
+ // ๐Ÿ”ฃ Material Symbols ligature name (AbIcons-backed AbIcon arrives with G2)
24
64
  icon: string;
65
+ // ๐Ÿšซ disabled (button: native `disabled`; link: `aria-disabled` + no href)
25
66
  disabled?: boolean;
67
+ // ๐Ÿ” grow a little on hover
26
68
  expands?: boolean;
69
+ // ๐Ÿค shrink a little when pressed
27
70
  shrinks?: boolean;
71
+ // โŒจ๏ธ custom tab index
28
72
  tabIndex?: number;
73
+ // ๐Ÿ–ฑ๏ธ click handler (button variant)
29
74
  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
75
+ // ๐Ÿ”— turns the button into a link
30
76
  href?: string;
77
+ // โœ… active / selected look
31
78
  active?: boolean;
79
+ // ๐Ÿซฅ no hover background
32
80
  naked?: boolean;
81
+ // โณ show a spinner instead of the icon (button variant)
33
82
  isLoading?: boolean;
83
+ // โ™ฟ G6: accessible name (preferred; translate it). Falls back to `title`.
84
+ label?: string;
85
+ // ๐Ÿ’ฌ tooltip text; also the accessible name when `label` is missing (pre-G6 callers)
34
86
  title?: string;
87
+ // ๐Ÿงพ button type
35
88
  type?: 'button' | 'submit' | 'reset';
36
89
  }
37
90
 
38
91
  const AbIconButton = forwardRef<HTMLButtonElement, AbIconButtonProps>(function AbIconButton(props, ref) {
92
+ // ๐Ÿงฉ destructure with defaults
39
93
  const {
40
- className,
41
- style,
42
- hidden = false,
43
- icon,
44
- disabled = false,
45
- expands = false,
46
- shrinks = false,
47
- tabIndex,
48
- onClick,
49
- href,
50
- active = false,
51
- naked = false,
52
- isLoading = false,
53
- title,
54
- type = 'button',
94
+ className, // ๐Ÿท๏ธ
95
+ style, // ๐ŸŽจ
96
+ hidden = false, // ๐Ÿ‘€
97
+ icon, // ๐Ÿ”ฃ
98
+ disabled = false, // ๐Ÿšฆ
99
+ expands = false, // ๐Ÿ”
100
+ shrinks = false, // ๐Ÿค
101
+ tabIndex, // โŒจ๏ธ
102
+ onClick, // ๐Ÿ–ฑ๏ธ
103
+ href, // ๐Ÿ”—
104
+ active = false, // โœ…
105
+ naked = false, // ๐Ÿซฅ
106
+ isLoading = false, // โณ
107
+ label, // โ™ฟ
108
+ title, // ๐Ÿ’ฌ
109
+ type = 'button', // ๐Ÿงพ
55
110
  } = props;
56
111
 
57
- const classNames = clsx('AbIconButton', 'icon-button', styles.abIconButton, className);
112
+ // โ™ฟ G6: one accessible name for both variants, ON the interactive element (not the wrapper span)
113
+ const name = label ?? title;
114
+ // ๐Ÿ’ฌ tooltip: explicit title wins, else reuse the name so sighted mouse users see it too
115
+ const tooltip = title ?? label;
116
+ // ๐Ÿท๏ธ classes
117
+ const classNames = clsx('AbIconButton', 'icon-button', styles.abIconButton, AB_MOTION.interactive, expands && AB_MOTION.lift, shrinks && AB_MOTION.press, className); // ๐ŸŽฌ hover lift / press from ab-nextjs-animations
118
+ // ๐Ÿ”ฃ the glyph is decoration: the ligature text ("arrow_back") must never be read aloud
119
+ const glyph = <span className={clsx('material-symbols-rounded', 'icon')} aria-hidden="true">{icon}</span>;
58
120
 
121
+ // ๐Ÿ”— link variant
59
122
  if (href && !isLoading) {
60
123
  return (
61
- <span hidden={hidden} className={styles.wrap} title={title}>
124
+ <span hidden={hidden} className={styles.wrap}>
62
125
  <a
63
- href={disabled ? undefined : href}
64
- role="button"
65
- className={classNames}
66
- style={style}
67
- data-active={String(active)}
68
- data-naked={String(naked)}
69
- data-expands={String(expands)}
70
- data-shrinks={String(shrinks)}
71
- tabIndex={tabIndex ?? 0}
72
- aria-disabled={disabled}
126
+ href={disabled ? undefined : href} // ๐Ÿšซ a disabled link has no target
127
+ className={classNames} // ๐Ÿท๏ธ
128
+ style={style} // ๐ŸŽจ
129
+ aria-label={name} // โ™ฟ G6: the href variant is named now
130
+ title={tooltip} // ๐Ÿ’ฌ on the link, not the wrapper
131
+ aria-disabled={disabled || undefined} // ๐Ÿšฆ
132
+ data-active={String(active)} // โœ…
133
+ data-naked={String(naked)} // ๐Ÿซฅ
134
+ data-expands={String(expands)} // ๐Ÿ”
135
+ data-shrinks={String(shrinks)} // ๐Ÿค
136
+ tabIndex={tabIndex ?? (disabled ? -1 : 0)} // โŒจ๏ธ a disabled link leaves the tab order
73
137
  >
74
- <span className={clsx('material-symbols-rounded', 'icon')}>{icon}</span>
138
+ {glyph}
75
139
  </a>
76
140
  </span>
77
141
  );
78
142
  }
79
143
 
144
+ // ๐Ÿ”˜ button variant
80
145
  return (
81
- <span hidden={hidden} className={styles.wrap} title={title}>
146
+ <span hidden={hidden} className={styles.wrap}>
82
147
  <button
83
- ref={ref}
84
- type={type}
85
- className={classNames}
86
- style={style}
87
- disabled={disabled || isLoading}
88
- data-active={String(active)}
89
- data-naked={String(naked)}
90
- data-expands={String(expands)}
91
- data-shrinks={String(shrinks)}
92
- onClick={onClick}
93
- tabIndex={tabIndex ?? 0}
94
- aria-label={title}
148
+ ref={ref} // ๐Ÿ”—
149
+ type={type} // ๐Ÿงพ
150
+ className={classNames} // ๐Ÿท๏ธ
151
+ style={style} // ๐ŸŽจ
152
+ disabled={disabled || isLoading} // ๐Ÿšฆ
153
+ aria-label={name} // โ™ฟ accessible name
154
+ title={tooltip} // ๐Ÿ’ฌ tooltip on the button itself
155
+ aria-busy={isLoading || undefined} // โณ announce loading
156
+ data-active={String(active)} // โœ…
157
+ data-naked={String(naked)} // ๐Ÿซฅ
158
+ data-expands={String(expands)} // ๐Ÿ”
159
+ data-shrinks={String(shrinks)} // ๐Ÿค
160
+ onClick={onClick} // ๐Ÿ–ฑ๏ธ
161
+ tabIndex={tabIndex ?? 0} // โŒจ๏ธ
95
162
  >
96
- {isLoading ? (
97
- <span className={clsx('spinner', 'dots-12', styles.spinner)} />
98
- ) : (
99
- <span className={clsx('material-symbols-rounded', 'icon')}>{icon}</span>
100
- )}
163
+ {isLoading ? <span className={clsx('spinner', 'dots-12', styles.spinner)} aria-hidden="true" /> : glyph}
101
164
  </button>
102
165
  </span>
103
166
  );
104
167
  });
105
168
 
169
+ // ๐Ÿ“ฆ exports
106
170
  export default AbIconButton;
107
171
  export { AbIconButton };
@@ -2,7 +2,32 @@
2
2
  * @license MIT
3
3
  * ~~~~~~~~~~~~
4
4
  * ab-nextjs-components
5
- * Plain CSS modules โ€” no Tailwind Tailwind apply (Turbopack-safe).
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: styles.module.css
28
+ * @file: ab-icon-button/styles.module.css
29
+ * @type: CSS Module
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
6
31
  */
7
32
 
8
33
  /*
@@ -11,6 +36,7 @@
11
36
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12
37
  */
13
38
 
39
+
14
40
  .wrap {
15
41
  display: inline-block;
16
42
  width: fit-content;
@@ -33,7 +59,7 @@
33
59
  cursor: pointer;
34
60
  background: transparent;
35
61
  color: inherit;
36
- transition: background-color 0.2s ease, transform 0.2s ease;
62
+ /* ๐ŸŽฌ motion: .ab-motion-interactive (+ .ab-motion-lift / .ab-motion-press) from ab-nextjs-animations */
37
63
  }
38
64
 
39
65
  .abIconButton:hover {
@@ -44,14 +70,6 @@
44
70
  background-color: transparent;
45
71
  }
46
72
 
47
- .abIconButton[data-expands='true']:hover {
48
- transform: scale(1.06);
49
- }
50
-
51
- .abIconButton[data-shrinks='true']:active {
52
- transform: scale(0.94);
53
- }
54
-
55
73
  .abIconButton:disabled {
56
74
  opacity: 0.5;
57
75
  cursor: not-allowed;
@@ -0,0 +1,62 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: AbInstallSnippet - Helpers (G35)
28
+ * @file: ab-install-snippet/helpers.ts
29
+ * @type: TypeScript (pure, server-safe)
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ */
32
+
33
+ /*
34
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
35
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
36
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
37
+ */
38
+
39
+ // ๐Ÿ“ฆ supported package managers
40
+ export type AbPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
41
+
42
+ // ๐Ÿ—‚๏ธ default tab order (mocks: pnpm first)
43
+ export const AB_PACKAGE_MANAGERS: readonly AbPackageManager[] = ['pnpm', 'npm', 'yarn', 'bun'] as const;
44
+
45
+ // ๐Ÿ” narrow an unknown (e.g. from localStorage) to a manager
46
+ export function isAbPackageManager(value: unknown): value is AbPackageManager {
47
+ return value === 'pnpm' || value === 'npm' || value === 'yarn' || value === 'bun'; // โœ…
48
+ }
49
+
50
+ /**
51
+ * @name getAbInstallCommand
52
+ * @description ๐Ÿงพ The install command for a manager. Package names are never translated.
53
+ * @example getAbInstallCommand('npm', 'ab-nextjs-core') // "npm i ab-nextjs-core"
54
+ */
55
+ export function getAbInstallCommand(pm: AbPackageManager, pkg: string, dev = false): string {
56
+ switch (pm) {
57
+ case 'npm': return `npm i ${dev ? '-D ' : ''}${pkg}`; // ๐Ÿ“ฆ
58
+ case 'yarn': return `yarn add ${dev ? '-D ' : ''}${pkg}`; // ๐Ÿงถ
59
+ case 'bun': return `bun add ${dev ? '-d ' : ''}${pkg}`; // ๐ŸฅŸ
60
+ default: return `pnpm add ${dev ? '-D ' : ''}${pkg}`; // โšก pnpm
61
+ }
62
+ }
@@ -0,0 +1,170 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the 'Software'), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * @project: ab-nextjs-components
27
+ * @name: AbInstallSnippet - Client (G35)
28
+ * @file: ab-install-snippet/index.tsx
29
+ * @type: Component (client) โ€” package-manager tabs + command + copy
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
+ *
32
+ * Example usage:
33
+ * 1+|> <AbInstallSnippet pkg="ab-nextjs-core" storageKey="ab-welcome-pm" labels={{ group: t('install.label', { pkg }), copy: t('install.copy'), copied: t('install.copied') }} />
34
+ */
35
+
36
+ /*
37
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
+ * MOTTO: We'll always do more ๐Ÿ˜œ!!!
39
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
40
+ */
41
+
42
+ 'use client';
43
+
44
+ // โš›๏ธ React
45
+ import type { CSSProperties, KeyboardEvent } from 'react';
46
+ import { useId, useRef, useState } from 'react';
47
+ // ๐Ÿงต classes
48
+ import clsx from 'clsx';
49
+ // ๐Ÿ“‹ clipboard + ๐Ÿ—„๏ธ persisted choice (hooks โ‰ฅ 0.1.6, deep imports)
50
+ import { useAbClipboard } from 'ab-nextjs-hooks/useAbClipboard';
51
+ import { useAbLocalStorage } from 'ab-nextjs-hooks/useAbLocalStorage';
52
+ // ๐Ÿ”˜ copy button (G6-fixed)
53
+ import AbIconButton from '../ab-icon-button';
54
+ // ๐Ÿงฎ pure helpers
55
+ import { AB_PACKAGE_MANAGERS, getAbInstallCommand, isAbPackageManager, type AbPackageManager } from './helpers';
56
+ // ๐ŸŽจ styles
57
+ import styles from './styles.module.css';
58
+
59
+ // ๐Ÿ“ฆ re-export helpers for convenience
60
+ export * from './helpers';
61
+
62
+ // ๐Ÿท๏ธ i18n labels
63
+ export interface AbInstallSnippetLabels {
64
+ group: string; // ๐Ÿ—‚๏ธ "Install ab-nextjs-core" (tablist name)
65
+ copy: string; // ๐Ÿ“‹ "Copy command"
66
+ copied: string; // โœ… "Copied" (announced politely)
67
+ }
68
+
69
+ // โš™๏ธ props
70
+ export interface AbInstallSnippetProps {
71
+ pkg: string; // ๐Ÿ“ฆ package name (not translated)
72
+ labels: AbInstallSnippetLabels; // ๐Ÿท๏ธ
73
+ managers?: readonly AbPackageManager[]; // ๐Ÿ—‚๏ธ default pnpm, npm, yarn, bun
74
+ storageKey?: string; // ๐Ÿ—„๏ธ persist + sync the choice across snippets (e.g. 'ab-welcome-pm')
75
+ defaultManager?: AbPackageManager; // ๐ŸŒฑ default 'pnpm'
76
+ dev?: boolean; // ๐Ÿ› ๏ธ dev dependency flag
77
+ className?: string; // ๐Ÿท๏ธ
78
+ style?: CSSProperties; // ๐ŸŽจ
79
+ }
80
+
81
+ // ๐Ÿง  state source: persisted (shared across instances) or local
82
+ type PmState = readonly [AbPackageManager, (pm: AbPackageManager) => void];
83
+
84
+ /** ๐Ÿ—„๏ธ persisted variant (hooks can't be conditional, so it's its own component) */
85
+ function PersistedSnippet(props: AbInstallSnippetProps & { storageKey: string }) {
86
+ const fallback = props.defaultManager ?? 'pnpm'; // ๐ŸŒฑ
87
+ const [raw, setRaw] = useAbLocalStorage<string>(props.storageKey, fallback); // ๐Ÿ—„๏ธ syncs every snippet with this key
88
+ const pm = isAbPackageManager(raw) ? raw : fallback; // ๐Ÿงฏ ignore junk in storage
89
+ return <SnippetView {...props} state={[pm, setRaw]} />;
90
+ }
91
+
92
+ /** ๐Ÿง  local variant */
93
+ function LocalSnippet(props: AbInstallSnippetProps) {
94
+ const [pm, setPm] = useState<AbPackageManager>(props.defaultManager ?? 'pnpm'); // ๐Ÿง 
95
+ return <SnippetView {...props} state={[pm, setPm]} />;
96
+ }
97
+
98
+ /** ๐Ÿ–ผ๏ธ the actual UI */
99
+ function SnippetView({ pkg, labels, managers = AB_PACKAGE_MANAGERS, dev = false, className, style, state }: AbInstallSnippetProps & { state: PmState }) {
100
+ const [pm, setPm] = state; // ๐ŸŽš๏ธ
101
+ const current = managers.includes(pm) ? pm : managers[0]; // ๐Ÿงฏ stored pm not offered here
102
+ const command = getAbInstallCommand(current, pkg, dev); // ๐Ÿงพ
103
+ const { copy, copied } = useAbClipboard(); // ๐Ÿ“‹
104
+ const base = useId(); // ๐Ÿ†” tab/panel ids
105
+ const tabs = useRef<Array<HTMLButtonElement | null>>([]); // ๐Ÿ“ roving focus
106
+
107
+ // โŒจ๏ธ APG tabs: arrows move + select (automatic activation), Home/End
108
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
109
+ const i = managers.indexOf(current); // ๐Ÿ“
110
+ const rtl = event.currentTarget.closest('[dir]')?.getAttribute('dir') === 'rtl'; // ๐ŸŒ
111
+ const nextKey = rtl ? 'ArrowLeft' : 'ArrowRight'; // โžก๏ธ
112
+ const prevKey = rtl ? 'ArrowRight' : 'ArrowLeft'; // โฌ…๏ธ
113
+ let j: number | null = null; // ๐ŸŽฏ
114
+ if (event.key === nextKey) j = (i + 1) % managers.length; // โญ๏ธ
115
+ else if (event.key === prevKey) j = (i - 1 + managers.length) % managers.length; // โฎ๏ธ
116
+ else if (event.key === 'Home') j = 0; // โฎ๏ธโฎ๏ธ
117
+ else if (event.key === 'End') j = managers.length - 1; // โญ๏ธโญ๏ธ
118
+ if (j === null) return; // ๐Ÿ™…
119
+ event.preventDefault(); // ๐Ÿ›‘
120
+ event.stopPropagation(); // ๐Ÿ›‘ the carousel must not also move
121
+ setPm(managers[j]); // โœ…
122
+ tabs.current[j]?.focus(); // ๐ŸŽฏ
123
+ };
124
+
125
+ return (
126
+ <div className={clsx('AbInstallSnippet', styles.root, className)} style={style} data-ab-part="install-snippet" data-ab-swipe-ignore="">
127
+ {/* ๐Ÿ—‚๏ธ manager tabs */}
128
+ <div role="tablist" aria-label={labels.group} className={styles.tabs} onKeyDown={onKeyDown}>
129
+ {managers.map((m, i) => (
130
+ <button
131
+ key={m} // ๐Ÿ”‘
132
+ ref={(el) => { tabs.current[i] = el; }} // ๐Ÿ“
133
+ id={`${base}-tab-${m}`} // ๐Ÿ†”
134
+ type="button" // ๐Ÿงพ
135
+ role="tab" // โ™ฟ
136
+ aria-selected={m === current} // โœ…
137
+ aria-controls={`${base}-panel`} // ๐Ÿ”—
138
+ tabIndex={m === current ? 0 : -1} // โŒจ๏ธ roving
139
+ className={styles.tab} // ๐ŸŽจ
140
+ onClick={() => setPm(m)} // ๐Ÿ–ฑ๏ธ
141
+ >
142
+ {m}
143
+ </button>
144
+ ))}
145
+ </div>
146
+ {/* ๐Ÿงพ command panel */}
147
+ <div id={`${base}-panel`} role="tabpanel" aria-labelledby={`${base}-tab-${current}`} className={styles.panel}>
148
+ <span className={styles.prompt} aria-hidden="true">$</span>{/* ๐Ÿ’ฒ decorative prompt */}
149
+ <code className={styles.code}>{command}</code>
150
+ <AbIconButton
151
+ icon={copied ? 'check' : 'content_copy'} // ๐Ÿ“‹ / โœ…
152
+ label={labels.copy} // โ™ฟ name stays "Copy command"
153
+ naked // ๐Ÿซฅ
154
+ className={styles.copy} // ๐ŸŽจ
155
+ onClick={() => { void copy(command); }} // ๐Ÿ“‹
156
+ />
157
+ </div>
158
+ {/* ๐Ÿ“ฃ own polite live region */}
159
+ <span className={styles.srOnly} aria-live="polite" role="status">{copied ? labels.copied : ''}</span>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ /** ๐Ÿ“ฆ Install box: pnpm / npm / yarn / bun tabs + command + copy + "Copied" announcement. */
165
+ export function AbInstallSnippet(props: AbInstallSnippetProps) {
166
+ return props.storageKey ? <PersistedSnippet {...props} storageKey={props.storageKey} /> : <LocalSnippet {...props} />;
167
+ }
168
+
169
+ // ๐Ÿ“ฆ default export
170
+ export default AbInstallSnippet;