@vunset/nuxt 0.0.0-stage → 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +83 -2
- package/dist/components.d.ts +13 -0
- package/dist/components.js +46 -0
- package/dist/module.d.ts +71 -0
- package/dist/module.js +270 -0
- package/dist/runtime/composables/use-infinity-fetch.d.ts +30 -0
- package/dist/runtime/composables/use-infinity-fetch.js +61 -0
- package/dist/runtime/icons.d.ts +2 -0
- package/dist/runtime/icons.js +11 -0
- package/dist/runtime/link.d.ts +2 -0
- package/dist/runtime/link.js +10 -0
- package/dist/runtime/route-query.d.ts +2 -0
- package/dist/runtime/route-query.js +18 -0
- package/dist/runtime/theme.d.ts +21 -0
- package/dist/runtime/theme.js +43 -0
- package/dist/runtime/toasts.d.ts +2 -0
- package/dist/runtime/toasts.js +40 -0
- package/dist/sprite.d.ts +19 -0
- package/dist/sprite.js +47 -0
- package/package.json +60 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vunset/nuxt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Nuxt module for Vunset. Every component and composable is available in your
|
|
4
|
+
pages without importing it, and the styles are already loaded.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @vunset/nuxt
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
[`@vunset/vue`](https://www.npmjs.com/package/@vunset/vue),
|
|
13
|
+
[`@vunset/ui`](https://www.npmjs.com/package/@vunset/ui) and
|
|
14
|
+
[`@vunset/utils`](https://www.npmjs.com/package/@vunset/utils) come with it.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// nuxt.config.ts
|
|
18
|
+
export default defineNuxtConfig({
|
|
19
|
+
modules: ['@vunset/nuxt']
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it:
|
|
24
|
+
|
|
25
|
+
```vue
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
const { setToast } = useToast();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<AppButton variant="solid" tone="accent" @click="setToast({ title: 'Saved' })">Save</AppButton>
|
|
32
|
+
<AppToaster />
|
|
33
|
+
</template>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Charts and data tables need one more package each. Install the ones you use,
|
|
37
|
+
and the components show up on their own:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @tanstack/charts # ChartArea, ChartBar, ChartDual, ChartDonut
|
|
41
|
+
npm install @tanstack/vue-table # DataTable, AppDataTable
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What you get
|
|
45
|
+
|
|
46
|
+
- Every component, headless (`DialogRoot`, `SelectTrigger`) and styled
|
|
47
|
+
(`AppButton`, `FormSelect`), loaded only on the pages that use it.
|
|
48
|
+
- The composables (`useToast`, `useTheme`, `useClipboard`, `useInfinityFetch`, …)
|
|
49
|
+
and the `@vunset/utils` helpers, auto-imported, in server routes too.
|
|
50
|
+
- The stylesheet, loaded before your own CSS.
|
|
51
|
+
- Icons from a folder of SVG files, or a sprite built from one.
|
|
52
|
+
- Light and dark mode, remembered across visits.
|
|
53
|
+
- Server rendering, toasts included.
|
|
54
|
+
|
|
55
|
+
## Options
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export default defineNuxtConfig({
|
|
59
|
+
modules: ['@vunset/nuxt'],
|
|
60
|
+
vunset: {
|
|
61
|
+
prefix: 'Vs'
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Option | Default | |
|
|
67
|
+
| ------------- | --------- | -------------------------------------------------------------------- |
|
|
68
|
+
| `prefix` | `''` | Goes in front of every component name: `'Vs'` gives `<VsAppButton>`. |
|
|
69
|
+
| `vue` | `true` | Registers the headless parts. |
|
|
70
|
+
| `ui` | `true` | Registers the styled components and loads their stylesheet. |
|
|
71
|
+
| `css` | with `ui` | Loads `@vunset/ui/all.css`. |
|
|
72
|
+
| `preflight` | `false` | Loads ui's CSS reset. Turn it on if you don't use Tailwind. |
|
|
73
|
+
| `composables` | `true` | Auto-imports the composables. |
|
|
74
|
+
| `utils` | `true` | Auto-imports the `@vunset/utils` helpers. |
|
|
75
|
+
| `icons` | see docs | Where your icons live: `{ dir, spriteDir }`, both `~/assets/icons`. |
|
|
76
|
+
| `theme` | `true` | Sets up light and dark mode. `false` leaves it to you. |
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
[github.com/vunset/vunset/tree/main/docs/nuxt](https://github.com/vunset/vunset/tree/main/docs/nuxt)
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Entry {
|
|
2
|
+
/** What the components are imported from. */
|
|
3
|
+
from: string;
|
|
4
|
+
/** The package it belongs to. */
|
|
5
|
+
package: string;
|
|
6
|
+
/** Optional peers the entry imports: without them installed, it is skipped. */
|
|
7
|
+
peers: string[];
|
|
8
|
+
components: string[];
|
|
9
|
+
composables: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const ENTRIES: Entry[];
|
|
12
|
+
/** `@vunset/utils`' helpers, imported in the app and on the server. */
|
|
13
|
+
export declare const UTILS: string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const ENTRIES = [
|
|
2
|
+
{
|
|
3
|
+
from: '@vunset/vue',
|
|
4
|
+
package: '@vunset/vue',
|
|
5
|
+
peers: [],
|
|
6
|
+
components: ['SwitchRoot', 'SwitchThumb', 'CheckboxRoot', 'CheckboxIndicator', 'CheckboxGroup', 'RadioGroup', 'RadioRoot', 'RadioIndicator', 'CollapsibleRoot', 'CollapsibleTrigger', 'CollapsiblePanel', 'AccordionRoot', 'AccordionItem', 'AccordionHeader', 'AccordionTrigger', 'AccordionPanel', 'PopoverRoot', 'PopoverTrigger', 'PopoverArrow', 'PopoverClose', 'PopoverTitle', 'PopoverDescription', 'Portal', 'Positioner', 'Popup', 'Addon', 'Separator', 'DialogRoot', 'DialogTrigger', 'DialogBackdrop', 'DialogTitle', 'DialogDescription', 'DialogClose', 'DrawerRoot', 'DrawerTrigger', 'DrawerBackdrop', 'DrawerHandle', 'DrawerTitle', 'DrawerDescription', 'DrawerClose', 'TooltipGroup', 'TooltipRoot', 'TooltipTrigger', 'TooltipArrow', 'FieldRoot', 'FieldLabel', 'FieldGroup', 'Input', 'Textarea', 'FieldDescription', 'FieldError', 'SelectRoot', 'SelectTrigger', 'SelectValue', 'SelectClear', 'SelectIndicator', 'SelectSearch', 'SelectList', 'SelectItem', 'SelectEmpty', 'SelectScrollIndicator', 'SelectPresets', 'ComboboxRoot', 'ComboboxTrigger', 'ComboboxChips', 'ComboboxChip', 'ComboboxChipRemove', 'ComboboxInput', 'ComboboxClear', 'ComboboxIndicator', 'ComboboxList', 'ComboboxItem', 'ComboboxEmpty', 'ComboboxScrollIndicator', 'ComboboxPresets', 'RangeRoot', 'RangeLabel', 'RangeValue', 'RangeControl', 'RangeTrack', 'RangeIndicator', 'RangeThumb', 'ToastViewport', 'ToastRoot', 'ToastTitle', 'ToastDescription', 'ToastClose', 'ToastAction', 'ToastProgress', 'Button', 'Badge', 'Icon', 'TableRoot', 'TableCaption', 'TableHeader', 'TableBody', 'TableFooter', 'TableRow', 'TableHeadCell', 'TableCell', 'AsyncState', 'DragRoot', 'DragItem', 'DragHandle', 'Copy', 'AlertRoot', 'AlertTitle', 'AlertDescription', 'Group', 'Appear', 'Deferred', 'Toggle', 'ToggleGroup', 'ProgressRoot', 'ProgressTrack', 'ProgressIndicator', 'ProgressValue', 'ProgressLabel', 'MeterRoot', 'MeterTrack', 'MeterIndicator', 'MeterValue', 'MeterLabel', 'PaginationRoot', 'PaginationPrev', 'PaginationNext', 'PaginationItem', 'PaginationEllipsis', 'FloatingRoot', 'FloatingHeader', 'FloatingTitle', 'FloatingClose', 'FloatingResizer', 'CalendarRoot', 'CalendarPrev', 'CalendarNext', 'CalendarViewTrigger', 'CalendarGrid', 'CalendarRow', 'CalendarWeek', 'CalendarWeekDay', 'CalendarDay', 'CalendarMonth', 'CalendarYear', 'CalendarPresets', 'CalendarPreset', 'DatepickerRoot', 'DatepickerTrigger', 'DatepickerValue', 'DatepickerClear', 'TimepickerRoot', 'TimepickerInput', 'TimepickerSegment', 'TimepickerTrigger', 'TimepickerClear', 'TimepickerColumn', 'TabsRoot', 'TabsList', 'TabsTab', 'TabsPanel', 'TabsIndicator', 'ScrollArea', 'ContextMenuRoot', 'ContextMenuTrigger', 'MenubarRoot', 'MenubarLink', 'MenuRoot', 'MenuTrigger', 'MenuItem', 'MenuCheckboxItem', 'MenuRadioGroup', 'MenuRadioItem', 'MenuGroup', 'MenuGroupLabel', 'MenuArrow', 'MenuItemIndicator', 'Part'],
|
|
7
|
+
composables: ['useToast', 'useClipboard', 'useClickOutside', 'useLocalStorage', 'useDownload', 'useExportCsv', 'useTheme', 'useValidate', 'useVirtualList']
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
from: '@vunset/vue/chart',
|
|
11
|
+
package: '@vunset/vue',
|
|
12
|
+
peers: ['@tanstack/charts'],
|
|
13
|
+
components: ['ChartRoot', 'ChartArea', 'ChartLine', 'ChartBar', 'ChartAxis', 'ChartGrid', 'ChartCrosshair', 'ChartTooltip', 'ChartLegend', 'ChartDonut'],
|
|
14
|
+
composables: []
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
from: '@vunset/vue/data-table',
|
|
18
|
+
package: '@vunset/vue',
|
|
19
|
+
peers: ['@tanstack/vue-table'],
|
|
20
|
+
components: ['DataTable'],
|
|
21
|
+
composables: []
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
from: '@vunset/ui',
|
|
25
|
+
package: '@vunset/ui',
|
|
26
|
+
peers: [],
|
|
27
|
+
components: ['FormField', 'FormInput', 'FormTextarea', 'FormCheckbox', 'FormCheckboxGroup', 'FormRadioGroup', 'FormRadio', 'FormSwitch', 'FormSelect', 'FormCombobox', 'FormRange', 'FormCalendar', 'FormDatepicker', 'FormTimepicker', 'FormToggleGroup', 'AppIcon', 'AppButton', 'AppCopy', 'AppBadge', 'AppAlert', 'AppProgress', 'AppMeter', 'AppSpinner', 'AppDotMatrix', 'AppEmptyState', 'AppAsyncState', 'AppCard', 'AppAppear', 'AppNumberFlow', 'AppTable', 'AppGroup', 'AppDialog', 'AppDrawer', 'AppPopover', 'AppConfirm', 'AppTooltip', 'AppTabs', 'AppCollapsible', 'AppAccordion', 'AppPagination', 'AppMenu', 'AppMenuItems', 'AppMenubar', 'AppContextMenu', 'AppFloating', 'AppToaster'],
|
|
28
|
+
composables: []
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
from: '@vunset/ui/chart',
|
|
32
|
+
package: '@vunset/ui',
|
|
33
|
+
peers: ['@tanstack/charts'],
|
|
34
|
+
components: ['AppChartArea', 'AppChartLine', 'AppChartBar', 'AppChartDonut'],
|
|
35
|
+
composables: []
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
from: '@vunset/ui/data-table',
|
|
39
|
+
package: '@vunset/ui',
|
|
40
|
+
peers: ['@tanstack/vue-table'],
|
|
41
|
+
components: ['AppDataTable'],
|
|
42
|
+
composables: []
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
/** `@vunset/utils`' helpers, imported in the app and on the server. */
|
|
46
|
+
export const UTILS = ['CHART_COLOR_PREFIX', 'chartColors', 'escapeCsvValue', 'formatCsv', 'DAYS_IN_WEEK', 'MONTHS_IN_YEAR', 'toDate', 'startOfDay', 'endOfDay', 'startOfMonth', 'endOfMonth', 'startOfYear', 'endOfYear', 'daysInMonth', 'addDays', 'addMonths', 'addYears', 'subDays', 'subMonths', 'subYears', 'compareDay', 'isSameDay', 'isSameMonth', 'isToday', 'isWeekend', 'isWithin', 'clampDate', 'monthNames', 'firstDayOfWeek', 'weekDayNames', 'formatDate', 'formatDateRange', 'formatISO', 'formatSafeISO', 'setDate', 'setDateRange', 'presetRange', 'debounce', 'toFixedNumber', 'calcTotal', 'calcAvg', 'calcPercentage', 'calcDiffPercentage', 'formatNumber', 'formatCurrency', 'formatPercentage', 'toSeparatedArray', 'toSeparatedString', 'toSeparatedList', 'getRandomString', 'generateRandomString', 'parseInlineSvg'];
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface ModuleOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Put before every component's name: `'Vs'` registers `<VsDialogRoot>` and
|
|
4
|
+
* `<VsAppButton>`. Empty, each keeps its own name.
|
|
5
|
+
* @default ''
|
|
6
|
+
*/
|
|
7
|
+
prefix?: string;
|
|
8
|
+
/** Registers `@vunset/vue`'s headless parts. @default true */
|
|
9
|
+
vue?: boolean;
|
|
10
|
+
/** Registers `@vunset/ui`'s components and adds its stylesheet. `false` leaves ui out. @default true */
|
|
11
|
+
ui?: boolean;
|
|
12
|
+
/** Adds `@vunset/ui/all.css`, before the app's own stylesheets. On with `ui`. */
|
|
13
|
+
css?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Adds `@vunset/ui/preflight.css`, before the app's own stylesheets: the reset
|
|
16
|
+
* the components are built on, for an app without Tailwind, whose Preflight is one.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
preflight?: boolean;
|
|
20
|
+
/** Auto-imports the public composables: `useToast`, `useTheme`, `useClipboard`, ... @default true */
|
|
21
|
+
composables?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Auto-imports `@vunset/utils`' helpers (`formatDate`, `formatNumber`,
|
|
24
|
+
* `calcTotal`, ...) in the app and in server routes, as a layer's `shared/utils` are.
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
utils?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Where `Icon` and `AppIcon` find their icons, handed to `configureIcons` for the app.
|
|
30
|
+
* `false` leaves icons to the app.
|
|
31
|
+
*/
|
|
32
|
+
icons?: IconsOptions | false;
|
|
33
|
+
/**
|
|
34
|
+
* Sets up `useTheme` before the app's components and provides it as `$theme`.
|
|
35
|
+
* With a `settings` object in `app.config` that has a `key`, the theme is
|
|
36
|
+
* `settings.theme`, and all of `settings` is kept in `localStorage` under that key.
|
|
37
|
+
* `false` leaves `useTheme` to the app. @default true
|
|
38
|
+
*/
|
|
39
|
+
theme?: boolean | ThemeOptions;
|
|
40
|
+
}
|
|
41
|
+
export interface ThemeOptions {
|
|
42
|
+
/** The choice before anything is stored. @default 'system' */
|
|
43
|
+
default?: 'light' | 'dark' | 'system';
|
|
44
|
+
/** The `localStorage` key, without `settings` in `app.config`; `null` remembers nothing. @default 'theme' */
|
|
45
|
+
storageKey?: string | null;
|
|
46
|
+
/** `<meta name="theme-color">` for each scheme; `settings.themeColor` in `app.config` wins. */
|
|
47
|
+
themeColor?: {
|
|
48
|
+
light: string;
|
|
49
|
+
dark: string;
|
|
50
|
+
};
|
|
51
|
+
/** Cross-fades the page on a change, where the browser can. @default true */
|
|
52
|
+
viewTransition?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface IconsOptions {
|
|
55
|
+
/**
|
|
56
|
+
* A folder of SVG files, drawn inline: `'~/assets/icons'`, or a path from the
|
|
57
|
+
* app's source folder. Several, and the first with a name wins.
|
|
58
|
+
* @default '~/assets/icons', where it exists
|
|
59
|
+
*/
|
|
60
|
+
dir?: string | string[];
|
|
61
|
+
/** Where the sprite is served from. @default '/sprite.svg' */
|
|
62
|
+
sprite?: string;
|
|
63
|
+
/**
|
|
64
|
+
* A folder of SVG files to build the sprite from, served at `sprite` and
|
|
65
|
+
* rebuilt as they change. Several, and the first with a name wins.
|
|
66
|
+
* @default '~/assets/icons', where it exists and `sprite` isn't set
|
|
67
|
+
*/
|
|
68
|
+
spriteDir?: string | string[];
|
|
69
|
+
}
|
|
70
|
+
declare const _default: import("@nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
71
|
+
export default _default;
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { addComponent, addImports, addPlugin, addServerImports, addTemplate, createResolver, defineNuxtModule, directoryToURL, resolveAlias, tryResolveModule, useLogger } from '@nuxt/kit';
|
|
6
|
+
import { ENTRIES, UTILS } from './components.js';
|
|
7
|
+
import { buildSprite, readIcons } from './sprite.js';
|
|
8
|
+
/** The kit's own packages: dependencies of the module, so an app lists the module alone. */
|
|
9
|
+
const OWN = ['@vunset/vue', '@vunset/ui', '@vunset/utils'];
|
|
10
|
+
const isOwn = (id) => OWN.some((name) => id === name || id.startsWith(`${name}/`));
|
|
11
|
+
const toPath = (resolved) => (resolved.startsWith('file:') ? fileURLToPath(resolved) : resolved);
|
|
12
|
+
/**
|
|
13
|
+
* An alias per entry a package exports, to the file the entry names: the app
|
|
14
|
+
* imports `@vunset/vue` without listing it, so its own resolution would not
|
|
15
|
+
* find it under pnpm or through a `file:` link. Longest first: Vite and Nitro
|
|
16
|
+
* take the first alias that matches, and a bare name matches its subpaths too.
|
|
17
|
+
*/
|
|
18
|
+
function exportAliases(name, resolved) {
|
|
19
|
+
let dir = dirname(toPath(resolved));
|
|
20
|
+
for (;;) {
|
|
21
|
+
const manifest = join(dir, 'package.json');
|
|
22
|
+
if (existsSync(manifest)) {
|
|
23
|
+
const json = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
24
|
+
if (json.name === name) {
|
|
25
|
+
const entries = Object.entries(json.exports ?? {});
|
|
26
|
+
entries.sort(([a], [b]) => b.length - a.length);
|
|
27
|
+
return Object.fromEntries(entries.map(([key, target]) => {
|
|
28
|
+
const file = typeof target === 'string' ? target : (target.import ?? target.default ?? '');
|
|
29
|
+
// `./css/*` maps a folder: the alias is its prefix.
|
|
30
|
+
return key.endsWith('/*')
|
|
31
|
+
? [`${name}${key.slice(1, -2)}`, join(dir, file.slice(0, -2))]
|
|
32
|
+
: [`${name}${key.slice(1)}`, join(dir, file)];
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir)
|
|
38
|
+
return {};
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Types an app uses without importing them, as Nuxt's own `Ref` is. The later entry wins: ui's, where an app may not list vue. */
|
|
43
|
+
const GLOBAL_TYPES = {
|
|
44
|
+
'@vunset/vue': [
|
|
45
|
+
'ListboxItem',
|
|
46
|
+
'ListboxOption',
|
|
47
|
+
'ListboxOptionValue',
|
|
48
|
+
'ToggleGroupValue',
|
|
49
|
+
'CheckboxGroupValue',
|
|
50
|
+
'RadioValue',
|
|
51
|
+
'CalendarValueInput',
|
|
52
|
+
'Toast',
|
|
53
|
+
'ToastOptions'
|
|
54
|
+
],
|
|
55
|
+
'@vunset/ui': [
|
|
56
|
+
'AppMenubarEntry',
|
|
57
|
+
'AppMenuEntry',
|
|
58
|
+
'AppToastData',
|
|
59
|
+
'AppTab',
|
|
60
|
+
'AppAccordionSection',
|
|
61
|
+
'FormToggleOption',
|
|
62
|
+
'FormCheckboxOption',
|
|
63
|
+
'FormRadioOption'
|
|
64
|
+
],
|
|
65
|
+
'@vunset/vue/data-table': ['DataTableColumn', 'DataTableColumnHelper'],
|
|
66
|
+
'@vunset/ui/data-table': ['DataTableColumn', 'DataTableColumnHelper']
|
|
67
|
+
};
|
|
68
|
+
/** lightningcss' `Features.LightDark`. */
|
|
69
|
+
const LIGHT_DARK = 1 << 20;
|
|
70
|
+
/** Where `dir` and `spriteDir` look when they aren't set. */
|
|
71
|
+
const ICONS_DIR = '~/assets/icons';
|
|
72
|
+
export default defineNuxtModule({
|
|
73
|
+
meta: {
|
|
74
|
+
name: '@vunset/nuxt',
|
|
75
|
+
configKey: 'vunset',
|
|
76
|
+
compatibility: { nuxt: '>=3.13.0' }
|
|
77
|
+
},
|
|
78
|
+
defaults: {
|
|
79
|
+
prefix: '',
|
|
80
|
+
theme: true,
|
|
81
|
+
vue: true,
|
|
82
|
+
composables: true,
|
|
83
|
+
utils: true
|
|
84
|
+
},
|
|
85
|
+
async setup(options, nuxt) {
|
|
86
|
+
const logger = useLogger('@vunset/nuxt');
|
|
87
|
+
const resolver = createResolver(import.meta.url);
|
|
88
|
+
const roots = [nuxt.options.rootDir, ...nuxt.options.modulesDir].map((dir) => directoryToURL(dir));
|
|
89
|
+
// The kit's packages are found from the module, which depends on them; a peer such as TanStack, from the app.
|
|
90
|
+
const resolveFrom = (id) => tryResolveModule(id, isOwn(id) ? [new URL(import.meta.url)] : roots);
|
|
91
|
+
const installed = async (id) => Boolean(await resolveFrom(id));
|
|
92
|
+
// Linked from elsewhere (`file:` a checkout) rather than installed into `node_modules`.
|
|
93
|
+
const linked = async (id) => {
|
|
94
|
+
const path = await resolveFrom(id);
|
|
95
|
+
if (!path)
|
|
96
|
+
return false;
|
|
97
|
+
try {
|
|
98
|
+
return !realpathSync(toPath(path)).includes(`${sep}node_modules${sep}`);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
for (const name of OWN) {
|
|
105
|
+
const path = await resolveFrom(name);
|
|
106
|
+
if (!path)
|
|
107
|
+
continue;
|
|
108
|
+
for (const [id, target] of Object.entries(exportAliases(name, path)))
|
|
109
|
+
nuxt.options.alias[id] ??= target;
|
|
110
|
+
}
|
|
111
|
+
const ui = options.ui ?? true;
|
|
112
|
+
if (ui && !(await installed('@vunset/ui'))) {
|
|
113
|
+
logger.warn('`ui` is on, but `@vunset/ui` is not installed: its components are left out.');
|
|
114
|
+
}
|
|
115
|
+
let missingPeers = false;
|
|
116
|
+
const types = new Map();
|
|
117
|
+
for (const entry of ENTRIES) {
|
|
118
|
+
if (entry.package === '@vunset/ui' && !ui)
|
|
119
|
+
continue;
|
|
120
|
+
if (!(await installed(entry.from)))
|
|
121
|
+
continue;
|
|
122
|
+
// A chart or data table needs its optional peers; without them, importing the entry fails.
|
|
123
|
+
if (!(await Promise.all(entry.peers.map(installed))).every(Boolean)) {
|
|
124
|
+
missingPeers = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// `vue: false` leaves out the headless parts, not the composables ui is used with.
|
|
128
|
+
const components = entry.package === '@vunset/vue' && !options.vue ? [] : entry.components;
|
|
129
|
+
for (const name of components) {
|
|
130
|
+
addComponent({
|
|
131
|
+
name: `${options.prefix}${name}`,
|
|
132
|
+
export: name,
|
|
133
|
+
filePath: entry.from,
|
|
134
|
+
// An app's own component of the same name wins, without a warning.
|
|
135
|
+
priority: -1
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (options.composables) {
|
|
139
|
+
addImports(entry.composables.map((name) => ({ name, from: entry.from })));
|
|
140
|
+
}
|
|
141
|
+
for (const name of GLOBAL_TYPES[entry.from] ?? [])
|
|
142
|
+
types.set(name, entry.from);
|
|
143
|
+
// Pre-bundled when the dev server starts, rather than found by the first page
|
|
144
|
+
// and reloaded. Not the chart or data table, which would pull TanStack into every app.
|
|
145
|
+
// A linked package is kept out: Vite would serve the copy it bundled once,
|
|
146
|
+
// through every rebuild, where served as source a rebuild shows at once.
|
|
147
|
+
const optimizeDeps = (nuxt.options.vite.optimizeDeps ??= {});
|
|
148
|
+
if (await linked(entry.from))
|
|
149
|
+
(optimizeDeps.exclude ??= []).push(entry.from);
|
|
150
|
+
else if (entry.peers.length === 0)
|
|
151
|
+
(optimizeDeps.include ??= []).push(entry.from);
|
|
152
|
+
}
|
|
153
|
+
addImports([...types].map(([name, from]) => ({ name, from, type: true })));
|
|
154
|
+
// Peers are looked for once, here: installing one restarts Nuxt, so its entry
|
|
155
|
+
// registers without the dev server being stopped and started by hand.
|
|
156
|
+
if (missingPeers && nuxt.options.dev)
|
|
157
|
+
nuxt.options.watch.push(join(nuxt.options.rootDir, 'package.json'));
|
|
158
|
+
if (options.composables) {
|
|
159
|
+
addImports({ name: 'useInfinityFetch', from: resolver.resolve('./runtime/composables/use-infinity-fetch') });
|
|
160
|
+
}
|
|
161
|
+
// Under the app's own: a helper of the same name in its `utils` or `shared` wins.
|
|
162
|
+
if (options.utils && (await installed('@vunset/utils'))) {
|
|
163
|
+
const imports = UTILS.map((name) => ({ name, from: '@vunset/utils', priority: -1 }));
|
|
164
|
+
addImports(imports);
|
|
165
|
+
addServerImports(imports);
|
|
166
|
+
}
|
|
167
|
+
if (await linked('@vunset/utils'))
|
|
168
|
+
((nuxt.options.vite.optimizeDeps ??= {}).exclude ??= []).push('@vunset/utils');
|
|
169
|
+
// A store per server request, carried to the page; see the plugin.
|
|
170
|
+
nuxt.options.build.transpile.push(resolver.resolve('./runtime'));
|
|
171
|
+
addPlugin(resolver.resolve('./runtime/toasts'));
|
|
172
|
+
addPlugin(resolver.resolve('./runtime/link'));
|
|
173
|
+
addPlugin(resolver.resolve('./runtime/route-query'));
|
|
174
|
+
if (options.theme !== false) {
|
|
175
|
+
const runtime = nuxt.options.runtimeConfig.public;
|
|
176
|
+
const theme = options.theme === true || options.theme === undefined ? {} : options.theme;
|
|
177
|
+
runtime.vunset = defu(runtime.vunset, { theme });
|
|
178
|
+
addPlugin(resolver.resolve('./runtime/theme'));
|
|
179
|
+
}
|
|
180
|
+
if (options.icons !== false)
|
|
181
|
+
addIcons(options.icons ?? {});
|
|
182
|
+
// Before the app's own, so its `components` rules come after ui's and win. ui
|
|
183
|
+
// declares Tailwind's layer order, so going first doesn't change how they rank.
|
|
184
|
+
if (ui && (options.css ?? true))
|
|
185
|
+
nuxt.options.css.unshift('@vunset/ui/all.css');
|
|
186
|
+
if (ui && options.preflight)
|
|
187
|
+
nuxt.options.css.unshift('@vunset/ui/preflight.css');
|
|
188
|
+
// Vite minifies CSS with lightningcss, which rewrites `light-dark()` for older
|
|
189
|
+
// browsers into variables only a `color-scheme` in the same CSS sets. One set
|
|
190
|
+
// on `<html>` by `useTheme()`, or none, leaves ui's colors invalid.
|
|
191
|
+
if (ui) {
|
|
192
|
+
const lightningcss = ((nuxt.options.vite.css ??= {}).lightningcss ??= {});
|
|
193
|
+
lightningcss.exclude = (lightningcss.exclude ?? 0) | LIGHT_DARK;
|
|
194
|
+
}
|
|
195
|
+
/** The folders an option names, as absolute paths; a missing one is reported. */
|
|
196
|
+
function folders(option) {
|
|
197
|
+
return [option ?? []].flat().map((folder) => {
|
|
198
|
+
const path = resolveAlias(folder);
|
|
199
|
+
const absolute = isAbsolute(path) ? path : resolve(nuxt.options.srcDir, path);
|
|
200
|
+
if (!existsSync(absolute))
|
|
201
|
+
logger.warn(`The icons folder \`${folder}\` doesn't exist.`);
|
|
202
|
+
return absolute;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** The default folder, when the app has it: unasked for, its absence isn't reported. */
|
|
206
|
+
function conventional() {
|
|
207
|
+
const path = resolveAlias(ICONS_DIR);
|
|
208
|
+
const absolute = isAbsolute(path) ? path : resolve(nuxt.options.srcDir, path);
|
|
209
|
+
return existsSync(absolute) ? [absolute] : [];
|
|
210
|
+
}
|
|
211
|
+
function addIcons({ dir, sprite, spriteDir }) {
|
|
212
|
+
const inlineDirs = dir === undefined ? conventional() : folders(dir);
|
|
213
|
+
// A `sprite` alone is one the app made itself: nothing is built over it.
|
|
214
|
+
const spriteDirs = spriteDir !== undefined ? folders(spriteDir) : sprite === undefined ? conventional() : [];
|
|
215
|
+
// Neither asked for nor there: the app's icons are its own business.
|
|
216
|
+
if (!options.icons && inlineDirs.length === 0 && spriteDirs.length === 0)
|
|
217
|
+
return;
|
|
218
|
+
// Nuxt's templates are virtual, so a glob can't be relative to one: it starts
|
|
219
|
+
// with `/`, from Vite's root, which is the app's source folder.
|
|
220
|
+
const root = nuxt.options.vite.root ?? nuxt.options.srcDir;
|
|
221
|
+
const globs = inlineDirs.map((folder) => {
|
|
222
|
+
const pattern = `/${relative(root, folder).replaceAll('\\', '/')}/*.svg`;
|
|
223
|
+
return `...import.meta.glob(${JSON.stringify(pattern)}, { query: '?raw', import: 'default' })`;
|
|
224
|
+
});
|
|
225
|
+
// Only the globs are written out: a glob takes no variable. Spread in order,
|
|
226
|
+
// so the first folder's file comes first, and is found first.
|
|
227
|
+
const template = addTemplate({
|
|
228
|
+
filename: 'vunset-icons.mjs',
|
|
229
|
+
getContents: () => `export const inline = ${globs.length > 0 ? `{ ${globs.join(', ')} }` : 'undefined'};\n`
|
|
230
|
+
});
|
|
231
|
+
nuxt.options.alias['#vunset/icons'] = template.dst;
|
|
232
|
+
const url = sprite ?? '/sprite.svg';
|
|
233
|
+
if (spriteDirs.length > 0)
|
|
234
|
+
addSprite(spriteDirs, url);
|
|
235
|
+
// The rest reaches the plugin through runtime config; an app's own value wins.
|
|
236
|
+
// A value, not `undefined`, which Nuxt would serve as `''` and no variable could set.
|
|
237
|
+
const runtime = nuxt.options.runtimeConfig.public;
|
|
238
|
+
runtime.vunset = defu(runtime.vunset, { icons: { sprite: url } });
|
|
239
|
+
addPlugin(resolver.resolve('./runtime/icons'));
|
|
240
|
+
}
|
|
241
|
+
// Built into the build folder and served from there, as a public asset: in
|
|
242
|
+
// development, and copied to whatever output the deploy preset makes.
|
|
243
|
+
function addSprite(sources, url) {
|
|
244
|
+
if (!url.startsWith('/')) {
|
|
245
|
+
logger.warn(`\`spriteDir\` builds a sprite the app serves, but \`sprite\` is \`${url}\`: give it a path.`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const dir = join(nuxt.options.buildDir, 'vunset-sprite');
|
|
249
|
+
const write = () => {
|
|
250
|
+
const sprite = buildSprite(readIcons(sources));
|
|
251
|
+
for (const name of sprite.invalid)
|
|
252
|
+
logger.warn(`The icon \`${name}.svg\` has no <svg> in it.`);
|
|
253
|
+
const file = join(dir, url);
|
|
254
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
255
|
+
writeFileSync(file, sprite.svg);
|
|
256
|
+
};
|
|
257
|
+
nuxt.hook('nitro:config', (config) => {
|
|
258
|
+
const nitro = config;
|
|
259
|
+
(nitro.publicAssets ??= []).push({ dir, maxAge: 0 });
|
|
260
|
+
});
|
|
261
|
+
// Not sooner: the CLI empties the build folder once Nitro is set up.
|
|
262
|
+
nuxt.hook('build:before', write);
|
|
263
|
+
nuxt.hook('builder:watch', (_event, path) => {
|
|
264
|
+
const file = resolve(nuxt.options.srcDir, path);
|
|
265
|
+
if (file.endsWith('.svg') && sources.some((source) => file.startsWith(source + sep)))
|
|
266
|
+
write();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ComponentPublicInstance, type MaybeRefOrGetter, type Ref } from 'vue';
|
|
2
|
+
import { type AsyncDataRequestStatus } from '#app';
|
|
3
|
+
export interface UseInfinityFetchOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The list: an element or a component (its root element), a template ref or a getter. The next page loads
|
|
6
|
+
* once its end is in view: its bottom within the window, and, when it scrolls itself (a table capped to a
|
|
7
|
+
* height, a panel), scrolled to its own end.
|
|
8
|
+
*/
|
|
9
|
+
el: MaybeRefOrGetter<Element | ComponentPublicInstance | null | undefined>;
|
|
10
|
+
/** How many pixels before the end the next page starts loading. @default 0 */
|
|
11
|
+
offset?: number;
|
|
12
|
+
/** Where the page's items are in the response, when it isn't the list itself. */
|
|
13
|
+
dataKey?: string;
|
|
14
|
+
/** `POST` sends the query, with `page`, as the body. @default 'GET' */
|
|
15
|
+
method?: 'GET' | 'POST';
|
|
16
|
+
/** Sent with every page; a change starts over from the first. */
|
|
17
|
+
query?: MaybeRefOrGetter<Record<string, unknown> | undefined>;
|
|
18
|
+
}
|
|
19
|
+
export interface UseInfinityFetchReturn<T> {
|
|
20
|
+
/** Every page loaded so far, in order. */
|
|
21
|
+
data: Ref<T[]>;
|
|
22
|
+
/** `end` once a page comes back empty. */
|
|
23
|
+
status: Ref<AsyncDataRequestStatus | 'end'>;
|
|
24
|
+
/** Starts over from the first page. */
|
|
25
|
+
refresh: () => void;
|
|
26
|
+
/** The next page to load. */
|
|
27
|
+
currentPage: Ref<number>;
|
|
28
|
+
}
|
|
29
|
+
/** A list fetched a page at a time, the next one as its end scrolls into view. */
|
|
30
|
+
export declare function useInfinityFetch<T>(url: string, options: UseInfinityFetchOptions): UseInfinityFetchReturn<T>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { computed, onMounted, onScopeDispose, ref, toValue, watch } from 'vue';
|
|
2
|
+
import { useRequestFetch } from '#app';
|
|
3
|
+
/** A list fetched a page at a time, the next one as its end scrolls into view. */
|
|
4
|
+
export function useInfinityFetch(url, options) {
|
|
5
|
+
// Untyped: the route is a runtime string, so Nuxt's typed routes can't check it.
|
|
6
|
+
const request = useRequestFetch();
|
|
7
|
+
const data = ref([]);
|
|
8
|
+
const page = ref(1);
|
|
9
|
+
const status = ref('idle');
|
|
10
|
+
const query = computed(() => toValue(options.query));
|
|
11
|
+
async function fetchPage() {
|
|
12
|
+
if (status.value === 'pending' || status.value === 'end')
|
|
13
|
+
return;
|
|
14
|
+
status.value = 'pending';
|
|
15
|
+
const params = { ...query.value, page: page.value };
|
|
16
|
+
const method = options.method ?? 'GET';
|
|
17
|
+
const response = await request(url, method === 'POST' ? { method, body: params } : { method, query: params });
|
|
18
|
+
const items = (options.dataKey ? response[options.dataKey] : response);
|
|
19
|
+
if (!items || items.length === 0) {
|
|
20
|
+
status.value = 'end';
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
data.value = [...data.value, ...items];
|
|
24
|
+
page.value += 1;
|
|
25
|
+
status.value = 'idle';
|
|
26
|
+
}
|
|
27
|
+
function refresh() {
|
|
28
|
+
page.value = 1;
|
|
29
|
+
data.value = [];
|
|
30
|
+
status.value = 'idle';
|
|
31
|
+
void fetchPage();
|
|
32
|
+
}
|
|
33
|
+
watch(query, refresh, { deep: true });
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
void fetchPage();
|
|
36
|
+
const check = () => {
|
|
37
|
+
const value = toValue(options.el);
|
|
38
|
+
const target = value instanceof Element ? value : value?.$el;
|
|
39
|
+
if (!(target instanceof Element))
|
|
40
|
+
return;
|
|
41
|
+
const offset = options.offset ?? 0;
|
|
42
|
+
// A list that doesn't scroll itself has nothing left to scroll, so only the window counts; a box capped
|
|
43
|
+
// to a height is always in the window, so only its own scroll counts.
|
|
44
|
+
const inWindow = target.getBoundingClientRect().bottom <= window.innerHeight + offset;
|
|
45
|
+
const scrolledToEnd = target.scrollHeight - target.scrollTop - target.clientHeight <= offset;
|
|
46
|
+
if (inWindow && scrolledToEnd)
|
|
47
|
+
void fetchPage();
|
|
48
|
+
};
|
|
49
|
+
// Captured, so a scroll inside any scrolling ancestor counts too.
|
|
50
|
+
window.addEventListener('scroll', check, { capture: true, passive: true });
|
|
51
|
+
window.addEventListener('resize', check, { passive: true });
|
|
52
|
+
// A page that leaves the end still in view loads the next at once. After the render, so the
|
|
53
|
+
// page's items are measured, not the list as it was before them.
|
|
54
|
+
watch(status, (value) => value === 'idle' && check(), { flush: 'post' });
|
|
55
|
+
onScopeDispose(() => {
|
|
56
|
+
window.removeEventListener('scroll', check, { capture: true });
|
|
57
|
+
window.removeEventListener('resize', check);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return { data, status, refresh, currentPage: page };
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from '#app';
|
|
2
|
+
import { configureIcons } from '@vunset/vue';
|
|
3
|
+
// The icon folders, globbed: written by the module, since a glob takes no variable.
|
|
4
|
+
import { inline } from '#vunset/icons';
|
|
5
|
+
export default defineNuxtPlugin({
|
|
6
|
+
name: 'vunset:icons',
|
|
7
|
+
setup() {
|
|
8
|
+
const { sprite } = useRuntimeConfig().public.vunset.icons;
|
|
9
|
+
configureIcons({ sprite: sprite || undefined, inline });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from '#app';
|
|
2
|
+
import { NuxtLink } from '#components';
|
|
3
|
+
import { configureLink } from '@vunset/vue';
|
|
4
|
+
// Every `to` the library turns into a link goes through NuxtLink, unless a component is told otherwise.
|
|
5
|
+
export default defineNuxtPlugin({
|
|
6
|
+
name: 'vunset:link',
|
|
7
|
+
setup() {
|
|
8
|
+
configureLink(NuxtLink);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNuxtPlugin, navigateTo, useRouter } from '#app';
|
|
2
|
+
import { routeQueryKey } from '@vunset/vue';
|
|
3
|
+
// The route a data table keeps its sort in, and a pagination its page, through Nuxt's router.
|
|
4
|
+
export default defineNuxtPlugin({
|
|
5
|
+
name: 'vunset:route-query',
|
|
6
|
+
setup(nuxtApp) {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
nuxtApp.vueApp.provide(routeQueryKey, {
|
|
9
|
+
get(key) {
|
|
10
|
+
const value = router.currentRoute.value.query[key];
|
|
11
|
+
return typeof value === 'string' ? value : undefined;
|
|
12
|
+
},
|
|
13
|
+
// `undefined` leaves the key out of the URL.
|
|
14
|
+
set: (key, value) => navigateTo({ query: { ...router.currentRoute.value.query, [key]: value } }),
|
|
15
|
+
setMany: (entries) => navigateTo({ query: { ...router.currentRoute.value.query, ...entries } })
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ThemeMode } from '@vunset/vue';
|
|
2
|
+
declare const _default: import("#app").Plugin<{
|
|
3
|
+
theme: {
|
|
4
|
+
current: "light" | "dark";
|
|
5
|
+
toggleTheme: () => void;
|
|
6
|
+
theme: ThemeMode;
|
|
7
|
+
resolved: "light" | "dark";
|
|
8
|
+
setTheme: (theme: ThemeMode) => void;
|
|
9
|
+
toggle: () => void;
|
|
10
|
+
};
|
|
11
|
+
}> & import("#app").ObjectPlugin<{
|
|
12
|
+
theme: {
|
|
13
|
+
current: "light" | "dark";
|
|
14
|
+
toggleTheme: () => void;
|
|
15
|
+
theme: ThemeMode;
|
|
16
|
+
resolved: "light" | "dark";
|
|
17
|
+
setTheme: (theme: ThemeMode) => void;
|
|
18
|
+
toggle: () => void;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineNuxtPlugin, updateAppConfig, useAppConfig, useRuntimeConfig } from '#app';
|
|
2
|
+
import { reactive, watch } from 'vue';
|
|
3
|
+
import { useLocalStorage, useTheme } from '@vunset/vue';
|
|
4
|
+
const MODES = ['light', 'dark', 'system'];
|
|
5
|
+
// Before any component asks for the theme, so these options are the ones it starts with.
|
|
6
|
+
export default defineNuxtPlugin({
|
|
7
|
+
name: 'vunset:theme',
|
|
8
|
+
setup() {
|
|
9
|
+
const options = useRuntimeConfig().public.vunset?.theme ?? {};
|
|
10
|
+
const config = useAppConfig();
|
|
11
|
+
const key = config.settings?.key;
|
|
12
|
+
// The whole of `settings` is remembered, not only the theme: sidebar, bookmarks, ...
|
|
13
|
+
if (import.meta.client && key) {
|
|
14
|
+
const { getItem, setItem } = useLocalStorage();
|
|
15
|
+
const saved = getItem(key);
|
|
16
|
+
if (saved)
|
|
17
|
+
updateAppConfig({ settings: saved });
|
|
18
|
+
watch(() => config.settings, (settings) => setItem(key, settings), { deep: true });
|
|
19
|
+
}
|
|
20
|
+
const theme = useTheme({
|
|
21
|
+
defaultTheme: options.default,
|
|
22
|
+
themeColor: config.settings?.themeColor ?? options.themeColor,
|
|
23
|
+
viewTransition: options.viewTransition ?? true,
|
|
24
|
+
...(key
|
|
25
|
+
? { storage: { get: () => config.settings.theme, set: (value) => (config.settings.theme = value) } }
|
|
26
|
+
: { storageKey: options.storageKey })
|
|
27
|
+
});
|
|
28
|
+
// `settings.theme` set by the app itself, or by `updateAppConfig`, is followed too.
|
|
29
|
+
if (key) {
|
|
30
|
+
watch(() => config.settings?.theme, (value) => {
|
|
31
|
+
if (MODES.includes(value) && value !== theme.theme.value)
|
|
32
|
+
theme.setTheme(value);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
provide: {
|
|
37
|
+
// Reactive, so a template reads `$theme.current` without `.value`. `current` and
|
|
38
|
+
// `toggleTheme` are the old layer's names for `resolved` and `toggle`.
|
|
39
|
+
theme: reactive({ ...theme, current: theme.resolved, toggleTheme: theme.toggle })
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useState } from '#app';
|
|
2
|
+
import { createToastStore, defaultToastStore, provideToastStore } from '@vunset/vue';
|
|
3
|
+
// A server render gets a store of its own, so a toast set while rendering one
|
|
4
|
+
// request reaches no other. What it holds is handed to the page in the payload,
|
|
5
|
+
// and put in the shared store there: the one `useToast()` reaches from anywhere,
|
|
6
|
+
// a plain module included.
|
|
7
|
+
export default defineNuxtPlugin({
|
|
8
|
+
name: 'vunset:toasts',
|
|
9
|
+
setup(nuxtApp) {
|
|
10
|
+
const carried = useState('vunset:toasts', () => []);
|
|
11
|
+
if (import.meta.server) {
|
|
12
|
+
const store = provideToastStore(createToastStore(), nuxtApp.vueApp);
|
|
13
|
+
nuxtApp.hook('app:rendered', () => {
|
|
14
|
+
carried.value = store.toasts.value.filter((toast) => toast.transitionStatus !== 'leave').map(plain);
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Before the app mounts, so the page hydrates with the toasts the server drew.
|
|
19
|
+
for (const toast of carried.value.slice().reverse())
|
|
20
|
+
defaultToastStore.add(toast);
|
|
21
|
+
carried.value = [];
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
/** What survives the payload: functions don't, so `onClose`, `slot` and a `data` holding one stay behind. */
|
|
25
|
+
function plain({ id, title, text, type, manualClose, timeout, data, onClose, slot }) {
|
|
26
|
+
const toast = { id, title, text, type, manualClose, timeout };
|
|
27
|
+
let dropped = Boolean(onClose || slot);
|
|
28
|
+
if (data !== undefined) {
|
|
29
|
+
try {
|
|
30
|
+
toast.data = structuredClone(data);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
dropped = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (dropped && import.meta.dev) {
|
|
37
|
+
console.warn(`[vunset] The toast \`${title ?? id}\` set during a server render reaches the page without its functions (\`onClose\`, \`slot\`, or one in \`data\`).`);
|
|
38
|
+
}
|
|
39
|
+
return toast;
|
|
40
|
+
}
|
package/dist/sprite.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SpriteIcon {
|
|
2
|
+
name: string;
|
|
3
|
+
svg: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Sprite {
|
|
6
|
+
svg: string;
|
|
7
|
+
/** The names drawn, in order. */
|
|
8
|
+
names: string[];
|
|
9
|
+
/** Files with no `<svg>` in them. */
|
|
10
|
+
invalid: string[];
|
|
11
|
+
}
|
|
12
|
+
/** The `.svg` files of each folder, in order; a name an earlier folder has is left out. */
|
|
13
|
+
export declare function readIcons(folders: string[]): SpriteIcon[];
|
|
14
|
+
/**
|
|
15
|
+
* One `<symbol>` per icon, parsed as an inline icon is: fills become
|
|
16
|
+
* `currentColor`, and the root's own attributes (`viewBox`, a stroke) move to
|
|
17
|
+
* the symbol, so a stroke icon keeps its stroke.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSprite(icons: SpriteIcon[]): Sprite;
|
package/dist/sprite.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { parseInlineSvg } from '@vunset/utils';
|
|
4
|
+
/** The `.svg` files of each folder, in order; a name an earlier folder has is left out. */
|
|
5
|
+
export function readIcons(folders) {
|
|
6
|
+
const icons = new Map();
|
|
7
|
+
for (const folder of folders) {
|
|
8
|
+
if (!existsSync(folder))
|
|
9
|
+
continue;
|
|
10
|
+
for (const file of readdirSync(folder).filter((file) => file.endsWith('.svg')).sort()) {
|
|
11
|
+
const name = basename(file, '.svg');
|
|
12
|
+
if (!icons.has(name))
|
|
13
|
+
icons.set(name, readFileSync(join(folder, file), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...icons].map(([name, svg]) => ({ name, svg }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One `<symbol>` per icon, parsed as an inline icon is: fills become
|
|
20
|
+
* `currentColor`, and the root's own attributes (`viewBox`, a stroke) move to
|
|
21
|
+
* the symbol, so a stroke icon keeps its stroke.
|
|
22
|
+
*/
|
|
23
|
+
export function buildSprite(icons) {
|
|
24
|
+
const names = [];
|
|
25
|
+
const invalid = [];
|
|
26
|
+
const symbols = [];
|
|
27
|
+
for (const { name, svg } of icons) {
|
|
28
|
+
const parsed = parseInlineSvg(svg);
|
|
29
|
+
if (!parsed) {
|
|
30
|
+
invalid.push(name);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// `Icon` draws a sprite icon with `fill="none"`, which a shape with no fill
|
|
34
|
+
// of its own would inherit through `<use>`: such a file is filled, as it is alone.
|
|
35
|
+
const attrs = { fill: 'currentColor', ...parsed.attrs };
|
|
36
|
+
const attributes = Object.entries(attrs)
|
|
37
|
+
.map(([key, value]) => ` ${key}="${value}"`)
|
|
38
|
+
.join('');
|
|
39
|
+
symbols.push(`<symbol id="${name}"${attributes}>${parsed.content.trim()}</symbol>`);
|
|
40
|
+
names.push(name);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" style="display: none">\n${symbols.join('\n')}\n</svg>\n`,
|
|
44
|
+
names,
|
|
45
|
+
invalid
|
|
46
|
+
};
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vunset/nuxt",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "Nuxt module for Vunset: every component, composable and helper auto-imported, styles loaded.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nuxt",
|
|
7
|
+
"nuxt-module",
|
|
8
|
+
"vue",
|
|
9
|
+
"headless",
|
|
10
|
+
"components",
|
|
11
|
+
"ui",
|
|
12
|
+
"accessibility"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/vunset/vunset.git",
|
|
18
|
+
"directory": "packages/nuxt"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/vunset/vunset#readme",
|
|
21
|
+
"bugs": "https://github.com/vunset/vunset/issues",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/module.d.ts",
|
|
29
|
+
"import": "./dist/module.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/components.ts && tsc -p tsconfig.build.json",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@vunset/ui": "1.0.0-rc.1",
|
|
38
|
+
"@vunset/utils": "1.0.0-rc.1",
|
|
39
|
+
"@vunset/vue": "1.0.0-rc.1",
|
|
40
|
+
"@nuxt/kit": "^4.6.1",
|
|
41
|
+
"defu": "^6.1.7"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@tanstack/charts": "^1.1.0",
|
|
45
|
+
"@tanstack/vue-table": "^9.2.4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@tanstack/charts": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@tanstack/vue-table": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@nuxt/schema": "^4.6.1",
|
|
57
|
+
"nuxt": "^4.6.1"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|