@arbor-education/design-system.components 3.1.0 → 3.2.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/.github/pull_request_template.md +4 -3
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +1 -0
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts +2 -2
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js +0 -1
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
- package/dist/components/icon/Icon.d.ts.map +1 -1
- package/dist/components/icon/Icon.js +18 -3
- package/dist/components/icon/Icon.js.map +1 -1
- package/dist/components/icon/Icon.stories.d.ts +4 -2
- package/dist/components/icon/Icon.stories.d.ts.map +1 -1
- package/dist/components/icon/Icon.stories.js +150 -102
- package/dist/components/icon/Icon.stories.js.map +1 -1
- package/dist/components/icon/Icon.test.js +146 -6
- package/dist/components/icon/Icon.test.js.map +1 -1
- package/dist/components/icon/LucideIcon.d.ts +17 -0
- package/dist/components/icon/LucideIcon.d.ts.map +1 -0
- package/dist/components/icon/LucideIcon.js +73 -0
- package/dist/components/icon/LucideIcon.js.map +1 -0
- package/dist/components/icon/allowedIcons.d.ts +44 -108
- package/dist/components/icon/allowedIcons.d.ts.map +1 -1
- package/dist/components/icon/allowedIcons.js +52 -112
- package/dist/components/icon/allowedIcons.js.map +1 -1
- package/dist/components/icon/types.d.ts +5 -4
- package/dist/components/icon/types.d.ts.map +1 -1
- package/dist/components/tabs/Tabs.d.ts +1 -1
- package/dist/components/tabs/Tabs.stories.d.ts +1 -1
- package/dist/components/tabs/Tabs.stories.js +1 -1
- package/dist/components/tabs/Tabs.stories.js.map +1 -1
- package/dist/components/tabs/TabsItem.d.ts +2 -2
- package/dist/components/tabs/TabsItem.d.ts.map +1 -1
- package/dist/components/tabs/TabsItem.stories.d.ts +5 -5
- package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -1
- package/dist/components/tabs/TabsItem.stories.js +7 -4
- package/dist/components/tabs/TabsItem.stories.js.map +1 -1
- package/package.json +1 -1
- package/src/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.tsx +2 -2
- package/src/components/icon/Icon.stories.tsx +224 -119
- package/src/components/icon/Icon.test.tsx +178 -6
- package/src/components/icon/Icon.tsx +25 -3
- package/src/components/icon/LucideIcon.tsx +94 -0
- package/src/components/icon/allowedIcons.tsx +67 -207
- package/src/components/icon/lucideDynamic.d.ts +13 -0
- package/src/components/icon/types.ts +5 -4
- package/src/components/tabs/Tabs.stories.tsx +1 -1
- package/src/components/tabs/TabsItem.stories.tsx +8 -4
- package/src/components/tabs/TabsItem.tsx +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useReducer } from 'react';
|
|
2
|
+
import { dynamicIconImports, iconNames } from 'lucide-react/dynamic.mjs';
|
|
3
|
+
import type { DynamicIconModule, IconName } from 'lucide-react/dynamic';
|
|
4
|
+
import type { CustomIconProps } from './types.js';
|
|
5
|
+
|
|
6
|
+
/** Every icon name published by `lucide-react`, kebab-cased. */
|
|
7
|
+
export type LucideIconName = IconName;
|
|
8
|
+
|
|
9
|
+
/** Every `lucide-react` icon name, for building galleries and validating input. */
|
|
10
|
+
export const lucideIconNames: readonly LucideIconName[] = iconNames;
|
|
11
|
+
|
|
12
|
+
type IconLoader = () => Promise<DynamicIconModule>;
|
|
13
|
+
type LucideIconComponent = DynamicIconModule['default'];
|
|
14
|
+
|
|
15
|
+
const iconLoaders: Readonly<Record<LucideIconName, IconLoader>> = dynamicIconImports;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Icons resolve through `lucide-react`'s per-icon dynamic imports so that consumers only ship the
|
|
19
|
+
* icons they actually render, rather than all ~1,900 of them. Both caches are module-scoped: the
|
|
20
|
+
* first render of a given icon anywhere in the app pays the import, and every render after that —
|
|
21
|
+
* including new instances in other trees — resolves synchronously from `loadedIcons`.
|
|
22
|
+
*/
|
|
23
|
+
const loadedIcons = new Map<LucideIconName, LucideIconComponent>();
|
|
24
|
+
const inFlightLoads = new Map<LucideIconName, Promise<void>>();
|
|
25
|
+
|
|
26
|
+
const loadIcon = (name: LucideIconName): Promise<void> => {
|
|
27
|
+
const inFlight = inFlightLoads.get(name);
|
|
28
|
+
if (inFlight) {
|
|
29
|
+
return inFlight;
|
|
30
|
+
}
|
|
31
|
+
// Annotated rather than inferred: names reaching `Icon` from a backend are not always valid.
|
|
32
|
+
const loader: IconLoader | undefined = iconLoaders[name];
|
|
33
|
+
if (!loader) {
|
|
34
|
+
console.error(`[design-system] "${name}" is not a known icon name.`);
|
|
35
|
+
// Cached so an unknown name is only reported once, however many times it is rendered.
|
|
36
|
+
const noSuchIcon = Promise.resolve();
|
|
37
|
+
inFlightLoads.set(name, noSuchIcon);
|
|
38
|
+
return noSuchIcon;
|
|
39
|
+
}
|
|
40
|
+
const load = loader()
|
|
41
|
+
.then((iconModule) => {
|
|
42
|
+
loadedIcons.set(name, iconModule.default);
|
|
43
|
+
})
|
|
44
|
+
.catch((error: unknown) => {
|
|
45
|
+
// Drop the rejected promise so a later render can retry (e.g. after a transient chunk 404).
|
|
46
|
+
inFlightLoads.delete(name);
|
|
47
|
+
console.error(`[design-system] Could not load the "${name}" icon.`, error);
|
|
48
|
+
});
|
|
49
|
+
inFlightLoads.set(name, load);
|
|
50
|
+
return load;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const useLucideIcon = (name: LucideIconName): LucideIconComponent | undefined => {
|
|
54
|
+
const [, rerender] = useReducer((renderCount: number) => renderCount + 1, 0);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (loadedIcons.has(name)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let isMounted = true;
|
|
61
|
+
void loadIcon(name).then(() => {
|
|
62
|
+
if (isMounted) {
|
|
63
|
+
rerender();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return () => {
|
|
67
|
+
isMounted = false;
|
|
68
|
+
};
|
|
69
|
+
}, [name]);
|
|
70
|
+
|
|
71
|
+
return loadedIcons.get(name);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type LucideIconProps = CustomIconProps & {
|
|
75
|
+
name: LucideIconName;
|
|
76
|
+
fill?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Renders a single `lucide-react` icon, loading it on demand. Until the icon resolves an empty SVG
|
|
81
|
+
* of the same size is rendered in its place, so the surrounding layout does not shift and the
|
|
82
|
+
* element keeps its accessible role throughout.
|
|
83
|
+
*/
|
|
84
|
+
export const LucideIcon = (props: LucideIconProps) => {
|
|
85
|
+
const { name, ...iconProps } = props;
|
|
86
|
+
const Component = useLucideIcon(name);
|
|
87
|
+
|
|
88
|
+
if (!Component) {
|
|
89
|
+
const { size, ...placeholderProps } = iconProps;
|
|
90
|
+
return <svg width={size} height={size} viewBox="0 0 24 24" {...placeholderProps} />;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return <Component {...iconProps} />;
|
|
94
|
+
};
|
|
@@ -1,217 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
ArrowDown,
|
|
4
|
-
ArrowDownUp,
|
|
5
|
-
ArrowLeft,
|
|
6
|
-
ArrowRight,
|
|
7
|
-
ArrowRightFromLine,
|
|
8
|
-
ArrowUp,
|
|
9
|
-
ArrowUpRight,
|
|
10
|
-
BookOpen,
|
|
11
|
-
CalendarFold,
|
|
12
|
-
Camera,
|
|
13
|
-
ChartColumnIncreasing,
|
|
14
|
-
ChartSpline,
|
|
15
|
-
Check,
|
|
16
|
-
ChevronDown,
|
|
17
|
-
ChevronLeft,
|
|
18
|
-
ChevronRight,
|
|
19
|
-
ChevronsLeft,
|
|
20
|
-
ChevronsRight,
|
|
21
|
-
ChevronUp,
|
|
22
|
-
CircleAlert,
|
|
23
|
-
CircleCheck,
|
|
24
|
-
CircleCheckBig,
|
|
25
|
-
CirclePercent,
|
|
26
|
-
CirclePlus,
|
|
27
|
-
CircleQuestionMark,
|
|
28
|
-
CircleX,
|
|
29
|
-
Clipboard,
|
|
30
|
-
ClipboardList,
|
|
31
|
-
Clock3,
|
|
32
|
-
CloudUpload,
|
|
33
|
-
Copy,
|
|
34
|
-
CornerDownLeft,
|
|
35
|
-
Dot,
|
|
36
|
-
Download,
|
|
37
|
-
Ellipsis,
|
|
38
|
-
EllipsisVertical,
|
|
39
|
-
Expand,
|
|
40
|
-
ExternalLink,
|
|
41
|
-
Eye,
|
|
42
|
-
EyeOff,
|
|
43
|
-
File,
|
|
44
|
-
Files,
|
|
45
|
-
Flag,
|
|
46
|
-
Funnel,
|
|
47
|
-
GraduationCap,
|
|
48
|
-
Grid3x3,
|
|
49
|
-
GripVertical,
|
|
50
|
-
History,
|
|
51
|
-
House,
|
|
52
|
-
Info,
|
|
53
|
-
IterationCcw,
|
|
54
|
-
LayoutList,
|
|
55
|
-
Lightbulb,
|
|
56
|
-
Link,
|
|
57
|
-
List,
|
|
58
|
-
ListFilterPlus,
|
|
59
|
-
ListTree,
|
|
60
|
-
LoaderCircle,
|
|
61
|
-
Lock,
|
|
62
|
-
LockOpen,
|
|
63
|
-
LogOut, type LucideProps,
|
|
64
|
-
Mail,
|
|
65
|
-
Menu,
|
|
66
|
-
MessageSquareMore,
|
|
67
|
-
Mic,
|
|
68
|
-
Minimize2,
|
|
69
|
-
Minus,
|
|
70
|
-
Monitor,
|
|
71
|
-
PaintBucket,
|
|
72
|
-
Paperclip,
|
|
73
|
-
PartyPopper,
|
|
74
|
-
Pencil,
|
|
75
|
-
Percent,
|
|
76
|
-
Phone,
|
|
77
|
-
Pin,
|
|
78
|
-
Plus,
|
|
79
|
-
Projector,
|
|
80
|
-
Redo2,
|
|
81
|
-
Save,
|
|
82
|
-
Search,
|
|
83
|
-
Send,
|
|
84
|
-
Settings,
|
|
85
|
-
Share,
|
|
86
|
-
Sheet,
|
|
87
|
-
SlidersHorizontal,
|
|
88
|
-
Smartphone,
|
|
89
|
-
Sparkles,
|
|
90
|
-
Star, SwitchCamera, Table, Trash2, TrendingDown, TrendingUp, TriangleAlert, Undo2, Upload, User,
|
|
91
|
-
Users,
|
|
92
|
-
UsersRound, X, Zap,
|
|
93
|
-
} from 'lucide-react';
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { LucideIcon, lucideIconNames, type LucideIconName } from './LucideIcon.js';
|
|
94
3
|
import { AskArbor } from './customIcons/AskArbor.js';
|
|
95
4
|
import { CheckSolid } from './customIcons/CheckSolid.js';
|
|
96
5
|
import { Google } from './customIcons/Google.js';
|
|
97
6
|
import { XSolid } from './customIcons/XSolid.js';
|
|
98
7
|
import type { CustomIconProps } from './types.js';
|
|
99
8
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'cloud-upload': CloudUpload,
|
|
132
|
-
'copy': Copy,
|
|
133
|
-
'corner-down-left': CornerDownLeft,
|
|
134
|
-
'date': CalendarFold,
|
|
135
|
-
'dot': Dot,
|
|
136
|
-
'download': Download,
|
|
137
|
-
'ellipsis-vertical': EllipsisVertical,
|
|
138
|
-
'ellipsis': Ellipsis,
|
|
139
|
-
'expand': Expand,
|
|
140
|
-
'external-link': ExternalLink,
|
|
141
|
-
'eye-off': EyeOff,
|
|
142
|
-
'eye': Eye,
|
|
143
|
-
'favourite-outline': Star,
|
|
144
|
-
'file': File,
|
|
145
|
-
'files': Files,
|
|
146
|
-
'flag': Flag,
|
|
147
|
-
'funnel': Funnel,
|
|
148
|
-
'grab': GripVertical,
|
|
149
|
-
'graduation-cap': GraduationCap,
|
|
150
|
-
'grid-3x3': Grid3x3,
|
|
151
|
-
'group': UsersRound,
|
|
152
|
-
'guardians': Users,
|
|
153
|
-
'history': History,
|
|
154
|
-
'house': House,
|
|
155
|
-
'info': Info,
|
|
156
|
-
'iteration-ccw': IterationCcw,
|
|
157
|
-
'layout-list': LayoutList,
|
|
158
|
-
'lightbulb': Lightbulb,
|
|
159
|
-
'link': Link,
|
|
160
|
-
'list-filter-plus': ListFilterPlus,
|
|
161
|
-
'list': List,
|
|
162
|
-
'list-tree': ListTree,
|
|
163
|
-
'loader': LoaderCircle,
|
|
164
|
-
'lock-open': LockOpen,
|
|
165
|
-
'lock': Lock,
|
|
166
|
-
'log-out': LogOut,
|
|
167
|
-
'mail': Mail,
|
|
168
|
-
'menu': Menu,
|
|
169
|
-
'message-square-more': MessageSquareMore,
|
|
170
|
-
'mic': Mic,
|
|
171
|
-
'minus': Minus,
|
|
172
|
-
'monitor': Monitor,
|
|
173
|
-
'paint-bucket': PaintBucket,
|
|
174
|
-
'paperclip': Paperclip,
|
|
175
|
-
'party-popper': PartyPopper,
|
|
176
|
-
'pencil': Pencil,
|
|
177
|
-
'percent': Percent,
|
|
178
|
-
'phone': Phone,
|
|
179
|
-
'pin': Pin,
|
|
180
|
-
'plus': Plus,
|
|
181
|
-
'projector': Projector,
|
|
182
|
-
'redo': Redo2,
|
|
183
|
-
'save': Save,
|
|
184
|
-
'search': Search,
|
|
185
|
-
'send': Send,
|
|
186
|
-
'settings': Settings,
|
|
187
|
-
'share': Share,
|
|
188
|
-
'sheet': Sheet,
|
|
189
|
-
'shrink': Minimize2,
|
|
190
|
-
'sliders-horizontal': SlidersHorizontal,
|
|
191
|
-
'smartphone': Smartphone,
|
|
192
|
-
'sorting': ArrowDownUp,
|
|
193
|
-
'sparkles': Sparkles,
|
|
194
|
-
'staff': GraduationCap,
|
|
195
|
-
'switch-camera': SwitchCamera,
|
|
196
|
-
'table': Table,
|
|
197
|
-
'trash': Trash2,
|
|
198
|
-
'trending-down': TrendingDown,
|
|
199
|
-
'trending-up': TrendingUp,
|
|
200
|
-
'triangle-alert': TriangleAlert,
|
|
201
|
-
'undo': Undo2,
|
|
202
|
-
'upload': Upload,
|
|
203
|
-
'user': User,
|
|
204
|
-
'x': X,
|
|
205
|
-
'zap': Zap,
|
|
206
|
-
// custom icons
|
|
9
|
+
/**
|
|
10
|
+
* Arbor names that point at a differently-named Lucide icon. These take precedence over the Lucide
|
|
11
|
+
* name they shadow, so `trash` stays the lidded can (`trash-2`) rather than Lucide's plain `trash`.
|
|
12
|
+
* Seven of these — `grab`, `group`, `loader`, `redo`, `shrink`, `trash` and `undo` — shadow a real
|
|
13
|
+
* Lucide icon with a different glyph, which is deliberate: renaming them would break every existing
|
|
14
|
+
* consumer. Reach for the target name directly if you want Lucide's version.
|
|
15
|
+
*
|
|
16
|
+
* The `satisfies` clause makes every target a compile-time-checked Lucide icon name.
|
|
17
|
+
*/
|
|
18
|
+
export const iconAliases = {
|
|
19
|
+
'3-dot': 'ellipsis-vertical',
|
|
20
|
+
'circle-help': 'circle-question-mark',
|
|
21
|
+
'date': 'calendar-fold',
|
|
22
|
+
'favourite-outline': 'star',
|
|
23
|
+
'grab': 'grip-vertical',
|
|
24
|
+
'group': 'users-round',
|
|
25
|
+
'guardians': 'users',
|
|
26
|
+
'loader': 'loader-circle',
|
|
27
|
+
'redo': 'redo-2',
|
|
28
|
+
'shrink': 'minimize-2',
|
|
29
|
+
'sorting': 'arrow-down-up',
|
|
30
|
+
'staff': 'graduation-cap',
|
|
31
|
+
'trash': 'trash-2',
|
|
32
|
+
'undo': 'undo-2',
|
|
33
|
+
} as const satisfies Readonly<Record<string, LucideIconName>>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Icons that are not plain Lucide glyphs: Arbor-specific artwork, and the solid variants built for
|
|
37
|
+
* particular semantic emphasis states.
|
|
38
|
+
*/
|
|
39
|
+
export const customIcons = {
|
|
207
40
|
'ask-arbor': AskArbor,
|
|
208
41
|
'google': Google,
|
|
209
|
-
|
|
210
|
-
'
|
|
211
|
-
|
|
212
|
-
<Star {...props} fill={props.color} />
|
|
42
|
+
'check-solid': CheckSolid,
|
|
43
|
+
'favourite-filled': (props: CustomIconProps) => (
|
|
44
|
+
<LucideIcon name="star" {...props} fill={props.color} />
|
|
213
45
|
),
|
|
214
|
-
'x-solid':
|
|
215
|
-
} as const
|
|
46
|
+
'x-solid': XSolid,
|
|
47
|
+
} as const satisfies Readonly<Record<string, (props: CustomIconProps) => ReactNode>>;
|
|
48
|
+
|
|
49
|
+
export type IconName
|
|
50
|
+
= | LucideIconName
|
|
51
|
+
| keyof typeof iconAliases
|
|
52
|
+
| keyof typeof customIcons;
|
|
53
|
+
|
|
54
|
+
// `Object.keys` widens to `string[]`, so the key unions are reasserted here.
|
|
55
|
+
const aliasNames = Object.keys(iconAliases) as ReadonlyArray<keyof typeof iconAliases>;
|
|
56
|
+
const customIconNames = Object.keys(customIcons) as ReadonlyArray<keyof typeof customIcons>;
|
|
57
|
+
|
|
58
|
+
/** Every name `Icon` accepts: the whole Lucide set, plus Arbor's aliases and custom icons. */
|
|
59
|
+
export const iconNames: readonly IconName[] = [
|
|
60
|
+
...lucideIconNames,
|
|
61
|
+
...aliasNames,
|
|
62
|
+
...customIconNames,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The `Icon` name allow-list, as a plain object so that membership can be tested with
|
|
67
|
+
* `hasOwnProperty`. Prefer {@link isIconName}; the values carry no meaning and exist only to make
|
|
68
|
+
* this a record. Icon components are no longer held here — they are loaded on demand, so nothing
|
|
69
|
+
* outside a render has a component to hand out.
|
|
70
|
+
*/
|
|
71
|
+
export const allowedIcons = Object.freeze(
|
|
72
|
+
Object.fromEntries(iconNames.map(name => [name, true])),
|
|
73
|
+
) as Readonly<Record<IconName, true>>;
|
|
216
74
|
|
|
217
|
-
|
|
75
|
+
/** Narrows an arbitrary string to an `IconName`, for validating names that come from a backend. */
|
|
76
|
+
export const isIconName = (name: string): name is IconName =>
|
|
77
|
+
Object.prototype.hasOwnProperty.call(allowedIcons, name);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lucide-react` ships its dynamic-import entry point as `dynamic.mjs` and declares no `exports`
|
|
3
|
+
* map, so the extensionless `lucide-react/dynamic` specifier only resolves in bundlers that list
|
|
4
|
+
* `.mjs` in `resolve.extensions`. `arbor-fe-library`'s webpack config does not, so we import the
|
|
5
|
+
* file by its exact name instead. TypeScript then looks for a sibling `dynamic.d.mts`, which the
|
|
6
|
+
* package does not publish — this declaration points it at the types that do ship.
|
|
7
|
+
*
|
|
8
|
+
* Type-only imports still use `lucide-react/dynamic`, which resolves under both `bundler` and
|
|
9
|
+
* classic `node` module resolution and is erased before any bundler sees it.
|
|
10
|
+
*/
|
|
11
|
+
declare module 'lucide-react/dynamic.mjs' {
|
|
12
|
+
export * from 'lucide-react/dynamic';
|
|
13
|
+
}
|
|
@@ -2,8 +2,9 @@ export const iconSizes = [12, 16, 24] as const;
|
|
|
2
2
|
export type IconSize = typeof iconSizes[number];
|
|
3
3
|
|
|
4
4
|
export type CustomIconProps = {
|
|
5
|
-
size: IconSize;
|
|
6
|
-
color?: string;
|
|
7
|
-
role?: string;
|
|
8
|
-
className?: string;
|
|
5
|
+
'size': IconSize;
|
|
6
|
+
'color'?: string;
|
|
7
|
+
'role'?: string;
|
|
8
|
+
'className'?: string;
|
|
9
|
+
'aria-hidden'?: boolean;
|
|
9
10
|
};
|
|
@@ -107,7 +107,7 @@ const DEVELOPER_NOTES = [
|
|
|
107
107
|
'| `active` | `boolean` | `false` | Marks this tab as selected — sets `aria-selected` and active styles |',
|
|
108
108
|
'| `tabElement` | `"button" \\| "link"` | `"button"` | Inner element type. `"button"` for JS-controlled tabs; `"link"` for anchor/router navigation |',
|
|
109
109
|
'| `tabElementProps` | `ButtonHTMLAttributes` or `AnchorHTMLAttributes` | — | Props forwarded to the inner `<button>` or `<a>`. Use `onClick` for state control; `href` for links |',
|
|
110
|
-
'| `iconName` | `
|
|
110
|
+
'| `iconName` | `IconName` | — | Optional icon rendered before the label text (16 px) |',
|
|
111
111
|
'| `children` | `ReactNode` | — | Tab label text |',
|
|
112
112
|
'| `className` | `string` | — | Extra CSS class on the `<li>` wrapper |',
|
|
113
113
|
'',
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@storybook/addon-docs/blocks';
|
|
10
10
|
import { TabsItem } from './TabsItem.js';
|
|
11
11
|
import { Tabs } from './Tabs.js';
|
|
12
|
-
import {
|
|
12
|
+
import type { IconName } from 'Components/icon/allowedIcons';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Docs page
|
|
@@ -93,10 +93,14 @@ const meta = {
|
|
|
93
93
|
},
|
|
94
94
|
iconName: {
|
|
95
95
|
control: 'select',
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Any Icon name works; these are the ones that read well in a tab.
|
|
97
|
+
options: [
|
|
98
|
+
'house', 'user', 'guardians', 'staff', 'date', 'sheet', 'table', 'settings',
|
|
99
|
+
'chart-column-increasing', 'clipboard-list', 'mail', 'archive',
|
|
100
|
+
] satisfies IconName[],
|
|
101
|
+
description: 'Optional icon rendered before the label text at 16 px. Accepts any `IconName`.',
|
|
98
102
|
table: {
|
|
99
|
-
type: { summary: '
|
|
103
|
+
type: { summary: 'IconName' },
|
|
100
104
|
defaultValue: { summary: 'undefined' },
|
|
101
105
|
},
|
|
102
106
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IconName } from 'Components/icon/allowedIcons';
|
|
3
3
|
import { Icon } from 'Components/icon/Icon';
|
|
4
4
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes } from 'react';
|
|
5
5
|
|
|
6
6
|
type TabsItemProps = {
|
|
7
7
|
active?: boolean;
|
|
8
|
-
iconName?:
|
|
8
|
+
iconName?: IconName;
|
|
9
9
|
} & (
|
|
10
10
|
| { tabElement?: 'button'; tabElementProps?: ButtonHTMLAttributes<HTMLButtonElement> }
|
|
11
11
|
| { tabElement?: 'link'; tabElementProps?: AnchorHTMLAttributes<HTMLAnchorElement> }
|