fluentui-extended 0.1.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/README.md ADDED
@@ -0,0 +1,239 @@
1
+ # FluentUI-Extended
2
+
3
+ Extended components for Fluent UI v9, designed to match Dynamics 365 patterns.
4
+
5
+ [![npm version](https://badge.fury.io/js/fluentui-extended.svg)](https://www.npmjs.com/package/fluentui-extended)
6
+ [![CI](https://github.com/garethcheyne/npm-fluentui-extended/actions/workflows/ci.yml/badge.svg)](https://github.com/garethcheyne/npm-fluentui-extended/actions)
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install fluentui-extended @fluentui/react-components @fluentui/react-icons
12
+ ```
13
+
14
+ ## Components
15
+
16
+ ### Lookup
17
+
18
+ A searchable dropdown component styled after Dynamics 365 lookup fields. Supports async search, expandable option details, and customizable header/footer.
19
+
20
+ ## Quick Start
21
+
22
+ ```tsx
23
+ import { Lookup, LookupOption } from 'fluentui-extended';
24
+ import { FluentProvider, webLightTheme } from '@fluentui/react-components';
25
+
26
+ const options: LookupOption[] = [
27
+ { key: '1', text: 'Contoso Ltd', secondaryText: 'CON001' },
28
+ { key: '2', text: 'Fabrikam Inc', secondaryText: 'FAB001' },
29
+ { key: '3', text: 'Adventure Works', secondaryText: 'ADV001' },
30
+ ];
31
+
32
+ function App() {
33
+ const [selected, setSelected] = useState<LookupOption | null>(null);
34
+
35
+ return (
36
+ <FluentProvider theme={webLightTheme}>
37
+ <Lookup
38
+ options={options}
39
+ selectedOption={selected}
40
+ onOptionSelect={setSelected}
41
+ placeholder="Search accounts..."
42
+ />
43
+ </FluentProvider>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ## Features
49
+
50
+ ### Basic Selection (with key)
51
+
52
+ ```tsx
53
+ const [selectedKey, setSelectedKey] = useState<string | null>(null);
54
+
55
+ <Lookup
56
+ options={options}
57
+ selectedKey={selectedKey}
58
+ onOptionSelect={(opt) => setSelectedKey(opt?.key ?? null)}
59
+ placeholder="Search..."
60
+ />
61
+ ```
62
+
63
+ ### Async Search (API Integration)
64
+
65
+ For async scenarios, use `selectedOption` to persist the display value when options change:
66
+
67
+ ```tsx
68
+ function AsyncLookup() {
69
+ const [options, setOptions] = useState<LookupOption[]>([]);
70
+ const [selected, setSelected] = useState<LookupOption | null>(null);
71
+ const [loading, setLoading] = useState(false);
72
+
73
+ const handleSearch = async (searchText: string) => {
74
+ setLoading(true);
75
+ const response = await fetch(`/api/accounts?search=${searchText}`);
76
+ setOptions(await response.json());
77
+ setLoading(false);
78
+ };
79
+
80
+ return (
81
+ <Lookup
82
+ options={options}
83
+ selectedOption={selected}
84
+ onOptionSelect={setSelected}
85
+ onSearchChange={handleSearch}
86
+ loading={loading}
87
+ searchDebounceMs={300}
88
+ placeholder="Type to search..."
89
+ />
90
+ );
91
+ }
92
+ ```
93
+
94
+ ### With Icons and Expandable Details
95
+
96
+ Options can include icons and expandable detail rows (click chevron to expand):
97
+
98
+ ```tsx
99
+ import { BuildingRegular } from '@fluentui/react-icons';
100
+
101
+ const options: LookupOption[] = [
102
+ {
103
+ key: '1',
104
+ text: 'Contoso Ltd',
105
+ secondaryText: 'CON001',
106
+ icon: <BuildingRegular />,
107
+ details: [
108
+ { label: 'Phone', value: '555-0100' },
109
+ { label: 'Industry', value: 'Technology' },
110
+ { value: 'Active Customer' },
111
+ ],
112
+ data: { id: 'acc-001', revenue: 5000000 }, // Custom data accessible in onOptionSelect
113
+ },
114
+ ];
115
+ ```
116
+
117
+ ### Dynamics 365 Style (Header & Footer)
118
+
119
+ ```tsx
120
+ import { Text, Button, Link } from '@fluentui/react-components';
121
+ import { AddRegular, PersonSearchRegular } from '@fluentui/react-icons';
122
+
123
+ <Lookup
124
+ options={options}
125
+ selectedOption={selected}
126
+ onOptionSelect={setSelected}
127
+ header={
128
+ <>
129
+ <Text size={200}>Accounts</Text>
130
+ <Button appearance="outline" size="small">Recent records</Button>
131
+ </>
132
+ }
133
+ footer={
134
+ <>
135
+ <Link style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
136
+ <AddRegular /> New
137
+ </Link>
138
+ <Link style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
139
+ <PersonSearchRegular /> Advanced
140
+ </Link>
141
+ </>
142
+ }
143
+ />
144
+ ```
145
+
146
+ ## API Reference
147
+
148
+ ### Lookup Props
149
+
150
+ | Prop | Type | Default | Description |
151
+ |------|------|---------|-------------|
152
+ | `options` | `LookupOption[]` | `[]` | Options to display in the dropdown |
153
+ | `selectedKey` | `string \| null` | - | Selected option key (controlled) |
154
+ | `selectedOption` | `LookupOption \| null` | - | Selected option object (recommended for async) |
155
+ | `onOptionSelect` | `(option: LookupOption \| null) => void` | - | Selection change callback |
156
+ | `onSearchChange` | `(searchText: string) => void` | - | Search text change callback |
157
+ | `placeholder` | `string` | `'Search...'` | Input placeholder |
158
+ | `loading` | `boolean` | `false` | Show loading spinner |
159
+ | `noResultsMessage` | `string` | `'No results found'` | Empty state message |
160
+ | `clearable` | `boolean` | `true` | Show clear button |
161
+ | `minSearchLength` | `number` | `0` | Min chars before search fires |
162
+ | `searchDebounceMs` | `number` | `300` | Search debounce delay (ms) |
163
+ | `header` | `ReactNode` | - | Header content |
164
+ | `footer` | `ReactNode` | - | Footer content |
165
+ | `disabled` | `boolean` | `false` | Disable the lookup |
166
+
167
+ *Also accepts all Fluent UI `Input` props except `onChange` and `value`.*
168
+
169
+ ### LookupOption
170
+
171
+ ```ts
172
+ interface LookupOption {
173
+ key: string; // Unique identifier (required)
174
+ text: string; // Display text (required)
175
+ secondaryText?: string; // Secondary line of text
176
+ icon?: ReactNode; // Icon component
177
+ details?: LookupOptionDetail[]; // Expandable details (chevron appears)
178
+ data?: unknown; // Custom data payload
179
+ disabled?: boolean; // Disable this option
180
+ }
181
+
182
+ interface LookupOptionDetail {
183
+ label?: string; // Optional label (e.g., "Phone:")
184
+ value: string; // Detail value
185
+ }
186
+ ```
187
+
188
+ ## Keyboard Navigation
189
+
190
+ | Key | Action |
191
+ |-----|--------|
192
+ | `↓` | Open dropdown / Move to next option |
193
+ | `↑` | Move to previous option |
194
+ | `Enter` | Select highlighted option |
195
+ | `Escape` | Close dropdown |
196
+ | `Tab` | Close dropdown and move focus |
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ # Install dependencies
202
+ npm install
203
+
204
+ # Run demo app
205
+ npm run demo
206
+
207
+ # Build library
208
+ npm run build
209
+
210
+ # Type check
211
+ npm run typecheck
212
+
213
+ # Watch mode
214
+ npm run dev
215
+ ```
216
+
217
+ ## GitHub Actions
218
+
219
+ This project includes CI/CD workflows:
220
+
221
+ - **CI** (`ci.yml`) - Runs on push/PR to main: type checking and build
222
+ - **Release** (`release.yml`) - Publishes to npm when a GitHub release is created
223
+
224
+ ### Setup npm Publishing
225
+
226
+ 1. Create an npm access token at [npmjs.com](https://www.npmjs.com/settings/~/tokens)
227
+ 2. Add it as a repository secret named `NPM_TOKEN` in GitHub Settings → Secrets
228
+
229
+ ## Contributing
230
+
231
+ 1. Fork the repository
232
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
233
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
234
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
235
+ 5. Open a Pull Request
236
+
237
+ ## License
238
+
239
+ MITMIT
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+ import { InputProps } from '@fluentui/react-components';
3
+
4
+ interface LookupOptionDetail {
5
+ /** Label for the detail row */
6
+ label?: string;
7
+ /** Value for the detail row */
8
+ value: string;
9
+ }
10
+ interface LookupOption {
11
+ /** Unique identifier for the option */
12
+ key: string;
13
+ /** Display text for the option */
14
+ text: string;
15
+ /** Optional secondary text */
16
+ secondaryText?: string;
17
+ /** Optional icon to display */
18
+ icon?: React.ReactNode;
19
+ /** Optional expandable details */
20
+ details?: LookupOptionDetail[];
21
+ /** Optional data associated with the option */
22
+ data?: unknown;
23
+ /** Whether the option is disabled */
24
+ disabled?: boolean;
25
+ }
26
+ interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
27
+ /** The options to display in the dropdown */
28
+ options?: LookupOption[];
29
+ /** Currently selected option key (use with options array lookup) */
30
+ selectedKey?: string | null;
31
+ /** Currently selected option (use for controlled selection with full data) */
32
+ selectedOption?: LookupOption | null;
33
+ /** Callback when selection changes - receives full option with data */
34
+ onOptionSelect?: (option: LookupOption | null) => void;
35
+ /** Callback when search text changes - use for async loading */
36
+ onSearchChange?: (searchText: string) => void;
37
+ /** Placeholder text when no selection */
38
+ placeholder?: string;
39
+ /** Whether the lookup is loading options */
40
+ loading?: boolean;
41
+ /** Message to display when no results found */
42
+ noResultsMessage?: string;
43
+ /** Whether to allow clearing the selection */
44
+ clearable?: boolean;
45
+ /** Minimum characters before triggering search */
46
+ minSearchLength?: number;
47
+ /** Debounce delay for search in milliseconds */
48
+ searchDebounceMs?: number;
49
+ /** Whether the dropdown should match the input width */
50
+ matchInputWidth?: boolean;
51
+ /** Header content rendered at the top of the dropdown */
52
+ header?: React.ReactNode;
53
+ /** Footer content rendered at the bottom of the dropdown */
54
+ footer?: React.ReactNode;
55
+ }
56
+
57
+ declare const Lookup: React.FC<LookupProps>;
58
+
59
+ export { Lookup, type LookupOption, type LookupOptionDetail, type LookupProps };
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+ import { InputProps } from '@fluentui/react-components';
3
+
4
+ interface LookupOptionDetail {
5
+ /** Label for the detail row */
6
+ label?: string;
7
+ /** Value for the detail row */
8
+ value: string;
9
+ }
10
+ interface LookupOption {
11
+ /** Unique identifier for the option */
12
+ key: string;
13
+ /** Display text for the option */
14
+ text: string;
15
+ /** Optional secondary text */
16
+ secondaryText?: string;
17
+ /** Optional icon to display */
18
+ icon?: React.ReactNode;
19
+ /** Optional expandable details */
20
+ details?: LookupOptionDetail[];
21
+ /** Optional data associated with the option */
22
+ data?: unknown;
23
+ /** Whether the option is disabled */
24
+ disabled?: boolean;
25
+ }
26
+ interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
27
+ /** The options to display in the dropdown */
28
+ options?: LookupOption[];
29
+ /** Currently selected option key (use with options array lookup) */
30
+ selectedKey?: string | null;
31
+ /** Currently selected option (use for controlled selection with full data) */
32
+ selectedOption?: LookupOption | null;
33
+ /** Callback when selection changes - receives full option with data */
34
+ onOptionSelect?: (option: LookupOption | null) => void;
35
+ /** Callback when search text changes - use for async loading */
36
+ onSearchChange?: (searchText: string) => void;
37
+ /** Placeholder text when no selection */
38
+ placeholder?: string;
39
+ /** Whether the lookup is loading options */
40
+ loading?: boolean;
41
+ /** Message to display when no results found */
42
+ noResultsMessage?: string;
43
+ /** Whether to allow clearing the selection */
44
+ clearable?: boolean;
45
+ /** Minimum characters before triggering search */
46
+ minSearchLength?: number;
47
+ /** Debounce delay for search in milliseconds */
48
+ searchDebounceMs?: number;
49
+ /** Whether the dropdown should match the input width */
50
+ matchInputWidth?: boolean;
51
+ /** Header content rendered at the top of the dropdown */
52
+ header?: React.ReactNode;
53
+ /** Footer content rendered at the bottom of the dropdown */
54
+ footer?: React.ReactNode;
55
+ }
56
+
57
+ declare const Lookup: React.FC<LookupProps>;
58
+
59
+ export { Lookup, type LookupOption, type LookupOptionDetail, type LookupProps };