flowbite-angular 20.0.6 → 20.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 CHANGED
@@ -56,13 +56,15 @@ npm i tailwindcss
56
56
  2. Install `flowbite-angular` as a dependency using `npm` by running the following command:
57
57
 
58
58
  ```bash
59
- npm i flowbite-angular
59
+ npm i flowbite-angular ng-primitives @ng-icons/core
60
60
  ```
61
61
 
62
62
  3. Import `flowbite-angular` inside your `style.css` file:
63
63
 
64
64
  ```css
65
- @import 'node_modules/flowbite-angular/styles/flowbite-angular.css';
65
+ @import 'tailwindcss';
66
+
67
+ @source "node_modules/flowbite-angular";
66
68
  ```
67
69
 
68
70
  # Components
@@ -0,0 +1,455 @@
1
+ import { createTheme, mergeDeep, colorToTheme } from 'flowbite-angular';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, inject, input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component, Directive } from '@angular/core';
4
+ import { createStateToken, createStateProvider, createStateInjector, createState } from 'ng-primitives/state';
5
+ import { NgTemplateOutlet } from '@angular/common';
6
+ import { twMerge } from 'tailwind-merge';
7
+
8
+ const flowbiteTableTheme = createTheme({
9
+ host: {
10
+ base: 'w-full text-left text-sm',
11
+ color: {
12
+ default: {
13
+ light: 'text-gray-500',
14
+ dark: 'dark:text-gray-400',
15
+ },
16
+ info: {
17
+ light: 'text-gray-500',
18
+ dark: 'dark:text-gray-400',
19
+ },
20
+ failure: {
21
+ light: 'text-gray-500',
22
+ dark: 'dark:text-gray-400',
23
+ },
24
+ success: {
25
+ light: 'text-gray-500',
26
+ dark: 'dark:text-gray-400',
27
+ },
28
+ warning: {
29
+ light: 'text-gray-500',
30
+ dark: 'dark:text-gray-400',
31
+ },
32
+ primary: {
33
+ light: 'text-gray-500',
34
+ dark: 'dark:text-gray-400',
35
+ },
36
+ },
37
+ },
38
+ });
39
+
40
+ const defaultFlowbiteTableConfig = {
41
+ baseTheme: flowbiteTableTheme,
42
+ color: 'default',
43
+ striped: false,
44
+ customTheme: {},
45
+ };
46
+ const FlowbiteTableConfigToken = new InjectionToken('FlowbiteTableConfigToken');
47
+ /**
48
+ * Provide the default Table configuration
49
+ * @param config The Table configuration
50
+ * @returns The provider
51
+ */
52
+ const provideFlowbiteTableConfig = (config) => [
53
+ {
54
+ provide: FlowbiteTableConfigToken,
55
+ useValue: { ...defaultFlowbiteTableConfig, ...config },
56
+ },
57
+ ];
58
+ /**
59
+ * Inject the Table configuration
60
+ * @see {@link defaultFlowbiteTableConfig}
61
+ * @returns The configuration
62
+ */
63
+ const injectFlowbiteTableConfig = () => inject(FlowbiteTableConfigToken, { optional: true }) ?? defaultFlowbiteTableConfig;
64
+
65
+ const FlowbiteTableStateToken = createStateToken('Flowbite Table');
66
+ const provideFlowbiteTableState = createStateProvider(FlowbiteTableStateToken);
67
+ const injectFlowbiteTableState = createStateInjector(FlowbiteTableStateToken);
68
+ const flowbiteTableState = createState(FlowbiteTableStateToken);
69
+
70
+ class Table {
71
+ constructor() {
72
+ this.config = injectFlowbiteTableConfig();
73
+ this.tableHead = input();
74
+ this.tableBody = input();
75
+ this.data = input();
76
+ this.tableFoot = input();
77
+ /**
78
+ * @see {@link injectFlowbiteTableConfig}
79
+ */
80
+ this.color = input(this.config.color);
81
+ /**
82
+ * @see {@link injectFlowbiteTableConfig}
83
+ */
84
+ this.striped = input(this.config.striped);
85
+ /**
86
+ * @see {@link injectFlowbiteTableConfig}
87
+ */
88
+ this.customTheme = input(this.config.customTheme);
89
+ this.theme = computed(() => {
90
+ const mergedTheme = mergeDeep(this.config.baseTheme, this.state.customTheme());
91
+ return {
92
+ host: {
93
+ root: twMerge(mergedTheme.host.base, colorToTheme(mergedTheme.host.color, this.state.color())),
94
+ },
95
+ };
96
+ });
97
+ /**
98
+ * @internal
99
+ */
100
+ this.state = flowbiteTableState(this);
101
+ }
102
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: Table, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
103
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: Table, isStandalone: true, selector: "\n table[flowbiteTable]\n ", inputs: { tableHead: { classPropertyName: "tableHead", publicName: "tableHead", isSignal: true, isRequired: false, transformFunction: null }, tableBody: { classPropertyName: "tableBody", publicName: "tableBody", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, tableFoot: { classPropertyName: "tableFoot", publicName: "tableFoot", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, customTheme: { classPropertyName: "customTheme", publicName: "customTheme", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "theme().host.root" } }, providers: [provideFlowbiteTableState()], exportAs: ["flowbiteTable"], ngImport: i0, template: `
104
+ <thead>
105
+ <ng-container [ngTemplateOutlet]="state.tableHead()" />
106
+ </thead>
107
+ <tbody>
108
+ @for (record of state.data(); track $index) {
109
+ <ng-container
110
+ [ngTemplateOutlet]="state.tableBody()"
111
+ [ngTemplateOutletContext]="{ $implicit: record }" />
112
+ }
113
+ </tbody>
114
+ <tfoot>
115
+ <ng-container [ngTemplateOutlet]="state.tableFoot()" />
116
+ </tfoot>
117
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
118
+ }
119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: Table, decorators: [{
120
+ type: Component,
121
+ args: [{
122
+ standalone: true,
123
+ selector: `
124
+ table[flowbiteTable]
125
+ `,
126
+ exportAs: 'flowbiteTable',
127
+ hostDirectives: [],
128
+ imports: [NgTemplateOutlet],
129
+ providers: [provideFlowbiteTableState()],
130
+ host: { '[class]': `theme().host.root` },
131
+ template: `
132
+ <thead>
133
+ <ng-container [ngTemplateOutlet]="state.tableHead()" />
134
+ </thead>
135
+ <tbody>
136
+ @for (record of state.data(); track $index) {
137
+ <ng-container
138
+ [ngTemplateOutlet]="state.tableBody()"
139
+ [ngTemplateOutletContext]="{ $implicit: record }" />
140
+ }
141
+ </tbody>
142
+ <tfoot>
143
+ <ng-container [ngTemplateOutlet]="state.tableFoot()" />
144
+ </tfoot>
145
+ `,
146
+ encapsulation: ViewEncapsulation.None,
147
+ changeDetection: ChangeDetectionStrategy.OnPush,
148
+ }]
149
+ }] });
150
+
151
+ const flowbiteTableHeadTheme = createTheme({
152
+ host: {
153
+ base: 'text-xs uppercase',
154
+ color: {
155
+ default: {
156
+ light: 'bg-gray-50 text-gray-700',
157
+ dark: 'dark:bg-gray-700 dark:text-gray-400',
158
+ },
159
+ info: {
160
+ light: 'bg-blue-50 text-blue-700',
161
+ dark: 'dark:bg-blue-700 dark:text-blue-400',
162
+ },
163
+ failure: {
164
+ light: 'bg-red-50 text-red-700',
165
+ dark: 'dark:bg-red-700 dark:text-red-300',
166
+ },
167
+ success: {
168
+ light: 'bg-green-50 text-green-700',
169
+ dark: 'dark:bg-green-700 dark:text-green-400',
170
+ },
171
+ warning: {
172
+ light: 'bg-yellow-50 text-yellow-700',
173
+ dark: 'dark:bg-yellow-700 dark:text-yellow-400',
174
+ },
175
+ primary: {
176
+ light: 'bg-primary-50 text-primary-700',
177
+ dark: 'dark:bg-primary-700 dark:text-primary-300',
178
+ },
179
+ },
180
+ },
181
+ });
182
+
183
+ const defaultFlowbiteTableHeadConfig = {
184
+ baseTheme: flowbiteTableHeadTheme,
185
+ customTheme: {},
186
+ };
187
+ const FlowbiteTableHeadConfigToken = new InjectionToken('FlowbiteTableHeadConfigToken');
188
+ /**
189
+ * Provide the default TableHead configuration
190
+ * @param config The TableHead configuration
191
+ * @returns The provider
192
+ */
193
+ const provideFlowbiteTableHeadConfig = (config) => [
194
+ {
195
+ provide: FlowbiteTableHeadConfigToken,
196
+ useValue: { ...defaultFlowbiteTableHeadConfig, ...config },
197
+ },
198
+ ];
199
+ /**
200
+ * Inject the TableHead configuration
201
+ * @see {@link defaultFlowbiteTableHeadConfig}
202
+ * @returns The configuration
203
+ */
204
+ const injectFlowbiteTableHeadConfig = () => inject(FlowbiteTableHeadConfigToken, { optional: true }) ?? defaultFlowbiteTableHeadConfig;
205
+
206
+ const FlowbiteTableHeadStateToken = createStateToken('Flowbite TableHead');
207
+ const provideFlowbiteTableHeadState = createStateProvider(FlowbiteTableHeadStateToken);
208
+ const injectFlowbiteTableHeadState = createStateInjector(FlowbiteTableHeadStateToken);
209
+ const flowbiteTableHeadState = createState(FlowbiteTableHeadStateToken);
210
+
211
+ class TableHead {
212
+ constructor() {
213
+ this.config = injectFlowbiteTableHeadConfig();
214
+ this.tableState = injectFlowbiteTableState();
215
+ /**
216
+ * @see {@link injectFlowbiteTableHeadConfig}
217
+ */
218
+ this.customTheme = input(this.config.customTheme);
219
+ this.theme = computed(() => {
220
+ const mergedTheme = mergeDeep(this.config.baseTheme, this.state.customTheme());
221
+ return {
222
+ host: {
223
+ root: twMerge(mergedTheme.host.base, colorToTheme(mergedTheme.host.color, this.tableState().color())),
224
+ },
225
+ };
226
+ });
227
+ /**
228
+ * @internal
229
+ */
230
+ this.state = flowbiteTableHeadState(this);
231
+ }
232
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableHead, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
233
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.6", type: TableHead, isStandalone: true, selector: "\n tr[flowbiteTableHead]\n ", inputs: { customTheme: { classPropertyName: "customTheme", publicName: "customTheme", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "theme().host.root" } }, providers: [provideFlowbiteTableHeadState()], exportAs: ["flowbiteTableHead"], ngImport: i0 }); }
234
+ }
235
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableHead, decorators: [{
236
+ type: Directive,
237
+ args: [{
238
+ standalone: true,
239
+ selector: `
240
+ tr[flowbiteTableHead]
241
+ `,
242
+ exportAs: 'flowbiteTableHead',
243
+ hostDirectives: [],
244
+ providers: [provideFlowbiteTableHeadState()],
245
+ host: { '[class]': `theme().host.root` },
246
+ }]
247
+ }] });
248
+
249
+ const flowbiteTableBodyTheme = createTheme({
250
+ host: {
251
+ base: 'border-b',
252
+ color: {
253
+ default: {
254
+ light: 'border-gray-200 data-striped:odd:bg-white data-striped:even:bg-gray-50',
255
+ dark: 'dark:border-gray-700 data-striped:odd:dark:bg-gray-900 data-striped:even:dark:bg-gray-800',
256
+ },
257
+ info: {
258
+ light: 'border-blue-200 data-striped:odd:bg-white data-striped:even:bg-blue-50',
259
+ dark: 'dark:border-blue-900 data-striped:odd:dark:bg-blue-950 data-striped:even:dark:bg-blue-900',
260
+ },
261
+ failure: {
262
+ light: 'border-red-200 data-striped:odd:bg-white data-striped:even:bg-red-50',
263
+ dark: 'dark:border-red-900 data-striped:odd:dark:bg-red-950 data-striped:even:dark:bg-red-900',
264
+ },
265
+ success: {
266
+ light: 'border-green-200 data-striped:odd:bg-white data-striped:even:bg-green-50',
267
+ dark: 'dark:border-green-900 data-striped:odd:dark:bg-green-950 data-striped:even:dark:bg-green-900',
268
+ },
269
+ warning: {
270
+ light: 'border-yellow-200 data-striped:odd:bg-white data-striped:even:bg-yellow-50',
271
+ dark: 'dark:border-yellow-900 data-striped:odd:dark:bg-yellow-950 data-striped:even:dark:bg-yellow-900',
272
+ },
273
+ primary: {
274
+ light: 'border-primary-200 data-striped:even:bg-primary-50 data-striped:odd:bg-white',
275
+ dark: 'dark:border-primary-900 data-striped:odd:dark:bg-primary-950 data-striped:even:dark:bg-primary-900',
276
+ },
277
+ },
278
+ },
279
+ });
280
+
281
+ const defaultFlowbiteTableBodyConfig = {
282
+ baseTheme: flowbiteTableBodyTheme,
283
+ customTheme: {},
284
+ };
285
+ const FlowbiteTableBodyConfigToken = new InjectionToken('FlowbiteTableBodyConfigToken');
286
+ /**
287
+ * Provide the default TableBody configuration
288
+ * @param config The TableBody configuration
289
+ * @returns The provider
290
+ */
291
+ const provideFlowbiteTableBodyConfig = (config) => [
292
+ {
293
+ provide: FlowbiteTableBodyConfigToken,
294
+ useValue: { ...defaultFlowbiteTableBodyConfig, ...config },
295
+ },
296
+ ];
297
+ /**
298
+ * Inject the TableBody configuration
299
+ * @see {@link defaultFlowbiteTableBodyConfig}
300
+ * @returns The configuration
301
+ */
302
+ const injectFlowbiteTableBodyConfig = () => inject(FlowbiteTableBodyConfigToken, { optional: true }) ?? defaultFlowbiteTableBodyConfig;
303
+
304
+ const FlowbiteTableBodyStateToken = createStateToken('Flowbite TableBody');
305
+ const provideFlowbiteTableBodyState = createStateProvider(FlowbiteTableBodyStateToken);
306
+ const injectFlowbiteTableBodyState = createStateInjector(FlowbiteTableBodyStateToken);
307
+ const flowbiteTableBodyState = createState(FlowbiteTableBodyStateToken);
308
+
309
+ class TableBody {
310
+ constructor() {
311
+ this.config = injectFlowbiteTableBodyConfig();
312
+ this.tableState = injectFlowbiteTableState();
313
+ /**
314
+ * @see {@link injectFlowbiteTableBodyConfig}
315
+ */
316
+ this.customTheme = input(this.config.customTheme);
317
+ this.theme = computed(() => {
318
+ const mergedTheme = mergeDeep(this.config.baseTheme, this.state.customTheme());
319
+ return {
320
+ host: {
321
+ root: twMerge(mergedTheme.host.base, colorToTheme(mergedTheme.host.color, this.tableState().color())),
322
+ },
323
+ };
324
+ });
325
+ /**
326
+ * @internal
327
+ */
328
+ this.state = flowbiteTableBodyState(this);
329
+ }
330
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableBody, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
331
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.6", type: TableBody, isStandalone: true, selector: "\n tr[flowbiteTableBody]\n ", inputs: { customTheme: { classPropertyName: "customTheme", publicName: "customTheme", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "theme().host.root", "attr.data-striped": "tableState().striped() || undefined" } }, providers: [provideFlowbiteTableBodyState()], exportAs: ["flowbiteTableBody"], ngImport: i0 }); }
332
+ }
333
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableBody, decorators: [{
334
+ type: Directive,
335
+ args: [{
336
+ standalone: true,
337
+ selector: `
338
+ tr[flowbiteTableBody]
339
+ `,
340
+ exportAs: 'flowbiteTableBody',
341
+ hostDirectives: [],
342
+ providers: [provideFlowbiteTableBodyState()],
343
+ host: {
344
+ '[class]': `theme().host.root`,
345
+ '[attr.data-striped]': 'tableState().striped() || undefined',
346
+ },
347
+ }]
348
+ }] });
349
+
350
+ const flowbiteTableFootTheme = createTheme({
351
+ host: {
352
+ base: 'font-semibold',
353
+ color: {
354
+ default: {
355
+ light: 'text-gray-900',
356
+ dark: 'dark:text-white',
357
+ },
358
+ info: {
359
+ light: 'text-gray-900',
360
+ dark: 'dark:text-white',
361
+ },
362
+ failure: {
363
+ light: 'text-gray-900',
364
+ dark: 'dark:text-white',
365
+ },
366
+ success: {
367
+ light: 'text-gray-900',
368
+ dark: 'dark:text-white',
369
+ },
370
+ warning: {
371
+ light: 'text-gray-900',
372
+ dark: 'dark:text-white',
373
+ },
374
+ primary: {
375
+ light: 'text-gray-900',
376
+ dark: 'dark:text-white',
377
+ },
378
+ },
379
+ },
380
+ });
381
+
382
+ const defaultFlowbiteTableFootConfig = {
383
+ baseTheme: flowbiteTableFootTheme,
384
+ customTheme: {},
385
+ };
386
+ const FlowbiteTableFootConfigToken = new InjectionToken('FlowbiteTableFootConfigToken');
387
+ /**
388
+ * Provide the default TableFoot configuration
389
+ * @param config The TableFoot configuration
390
+ * @returns The provider
391
+ */
392
+ const provideFlowbiteTableFootConfig = (config) => [
393
+ {
394
+ provide: FlowbiteTableFootConfigToken,
395
+ useValue: { ...defaultFlowbiteTableFootConfig, ...config },
396
+ },
397
+ ];
398
+ /**
399
+ * Inject the TableFoot configuration
400
+ * @see {@link defaultFlowbiteTableFootConfig}
401
+ * @returns The configuration
402
+ */
403
+ const injectFlowbiteTableFootConfig = () => inject(FlowbiteTableFootConfigToken, { optional: true }) ?? defaultFlowbiteTableFootConfig;
404
+
405
+ const FlowbiteTableFootStateToken = createStateToken('Flowbite TableFoot');
406
+ const provideFlowbiteTableFootState = createStateProvider(FlowbiteTableFootStateToken);
407
+ const injectFlowbiteTableFootState = createStateInjector(FlowbiteTableFootStateToken);
408
+ const flowbiteTableFootState = createState(FlowbiteTableFootStateToken);
409
+
410
+ class TableFoot {
411
+ constructor() {
412
+ this.config = injectFlowbiteTableFootConfig();
413
+ this.tableState = injectFlowbiteTableState();
414
+ /**
415
+ * @see {@link injectFlowbiteTableFootConfig}
416
+ */
417
+ this.customTheme = input(this.config.customTheme);
418
+ this.theme = computed(() => {
419
+ const mergedTheme = mergeDeep(this.config.baseTheme, this.state.customTheme());
420
+ return {
421
+ host: {
422
+ root: twMerge(mergedTheme.host.base, colorToTheme(mergedTheme.host.color, this.tableState().color())),
423
+ },
424
+ };
425
+ });
426
+ /**
427
+ * @internal
428
+ */
429
+ this.state = flowbiteTableFootState(this);
430
+ }
431
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableFoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
432
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.6", type: TableFoot, isStandalone: true, selector: "\n tr[flowbiteTableFoot]\n ", inputs: { customTheme: { classPropertyName: "customTheme", publicName: "customTheme", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "theme().host.root" } }, providers: [provideFlowbiteTableFootState()], exportAs: ["flowbiteTableFoot"], ngImport: i0 }); }
433
+ }
434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: TableFoot, decorators: [{
435
+ type: Directive,
436
+ args: [{
437
+ standalone: true,
438
+ selector: `
439
+ tr[flowbiteTableFoot]
440
+ `,
441
+ exportAs: 'flowbiteTableFoot',
442
+ hostDirectives: [],
443
+ providers: [provideFlowbiteTableFootState()],
444
+ host: { '[class]': `theme().host.root` },
445
+ }]
446
+ }] });
447
+
448
+ /* Table */
449
+
450
+ /**
451
+ * Generated bundle index. Do not edit.
452
+ */
453
+
454
+ export { FlowbiteTableBodyConfigToken, FlowbiteTableBodyStateToken, FlowbiteTableConfigToken, FlowbiteTableFootConfigToken, FlowbiteTableFootStateToken, FlowbiteTableHeadConfigToken, FlowbiteTableHeadStateToken, FlowbiteTableStateToken, Table, TableBody, TableFoot, TableHead, defaultFlowbiteTableBodyConfig, defaultFlowbiteTableConfig, defaultFlowbiteTableFootConfig, defaultFlowbiteTableHeadConfig, flowbiteTableBodyState, flowbiteTableBodyTheme, flowbiteTableFootState, flowbiteTableFootTheme, flowbiteTableHeadState, flowbiteTableHeadTheme, flowbiteTableState, flowbiteTableTheme, injectFlowbiteTableBodyConfig, injectFlowbiteTableBodyState, injectFlowbiteTableConfig, injectFlowbiteTableFootConfig, injectFlowbiteTableFootState, injectFlowbiteTableHeadConfig, injectFlowbiteTableHeadState, injectFlowbiteTableState, provideFlowbiteTableBodyConfig, provideFlowbiteTableBodyState, provideFlowbiteTableConfig, provideFlowbiteTableFootConfig, provideFlowbiteTableFootState, provideFlowbiteTableHeadConfig, provideFlowbiteTableHeadState, provideFlowbiteTableState };
455
+ //# sourceMappingURL=flowbite-angular-table.mjs.map