@plutocms/pluto 0.11.0 → 0.13.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/CHANGELOG.md +36 -0
- package/FEATURES.md +13 -0
- package/README.md +30 -0
- package/app/assets/css/admin-theme.css +216 -0
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/ColorModeButton.vue +4 -2
- package/app/components/Pluto/Content/PlutoContentForm.vue +1 -0
- package/app/components/Pluto/Content/PlutoContentList.vue +122 -20
- package/app/components/Pluto/Dashboard/PlutoDashboardContentActivity.vue +114 -0
- package/app/components/Pluto/Dashboard/PlutoDashboardContentOverview.vue +103 -0
- package/app/components/Pluto/Dashboard/PlutoDashboardQuickActions.vue +70 -0
- package/app/components/Pluto/Navbar/PlutoNavbarAdmin.vue +9 -1
- package/app/components/Pluto/View/PlutoViewAdminPanel.vue +105 -19
- package/app/components/Pluto/View/PlutoViewAside.vue +26 -9
- package/app/components/modal/Modal.vue +1 -1
- package/app/composables/pluto-admin-theme.ts +14 -0
- package/app/composables/pluto-command-palette.ts +59 -0
- package/app/composables/pluto-content-activity.ts +37 -0
- package/app/composables/pluto-content-list.ts +88 -7
- package/app/composables/pluto-content-stats.ts +78 -0
- package/app/composables/pluto-view.ts +9 -0
- package/app/layouts/admin.vue +30 -6
- package/app/layouts/auth.vue +4 -2
- package/app/pages/admin/home.vue +34 -7
- package/app/plugins/pluto-core-registrations.ts +34 -0
- package/modules/pluto-css/css-entries.ts +182 -0
- package/modules/pluto-css/index.ts +86 -0
- package/nuxt.config.ts +3 -1
- package/package.json +2 -1
- package/scripts/generate-admin-theme.cjs +110 -0
- package/server/api/_pluto/content/[type]/index.get.ts +4 -1
- package/server/api/_pluto/content-activity/[type].get.ts +15 -0
- package/server/utils/pluto-content-handlers.ts +65 -1
- package/shared/types/content.ts +24 -0
- package/app/assets/css/tailwind.css +0 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.13.0](https://github.com/plutocms/pluto/compare/v0.12.0...v0.13.0) (2026-10-18)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **admin:** mount the aside once and keep it mounted on mobile ([#158](https://github.com/plutocms/pluto/issues/158)) ([dcca464](https://github.com/plutocms/pluto/commit/dcca464ead4ec5f46b2c26c0eea0f94260553ac9))
|
|
10
|
+
* **content:** page and virtualize long content lists ([#154](https://github.com/plutocms/pluto/issues/154)) ([b8d35b0](https://github.com/plutocms/pluto/commit/b8d35b05230169713a3e7cacbdfd95f52c72936a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **admin:** darken the navbar and sidebar in dark mode ([#159](https://github.com/plutocms/pluto/issues/159)) ([011fd2f](https://github.com/plutocms/pluto/commit/011fd2f616daf961486e99f69a0e8f0f0db5e51f))
|
|
16
|
+
* **admin:** make the navbar near black in light mode too ([#160](https://github.com/plutocms/pluto/issues/160)) ([f39656a](https://github.com/plutocms/pluto/commit/f39656a5b52081c05e8128b4d0828970b96a14e5))
|
|
17
|
+
* **admin:** use a dim dark gray instead of pitch black in dark mode ([#156](https://github.com/plutocms/pluto/issues/156)) ([500c900](https://github.com/plutocms/pluto/commit/500c900783980d760d6fade406dfc15334a61a4e))
|
|
18
|
+
* **admin:** use the theme text color in the modal ([#157](https://github.com/plutocms/pluto/issues/157)) ([e73c613](https://github.com/plutocms/pluto/commit/e73c613eead017fabbae66ef6f6a7624ec5a5a04))
|
|
19
|
+
|
|
20
|
+
## [0.12.0](https://github.com/plutocms/pluto/compare/v0.11.0...v0.12.0) (2026-10-17)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### ⚠ BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* **admin:** An app with its own Tailwind entry must import @nuxt/ui and #build/pluto.css after tailwindcss:
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* **admin:** add a Cmd/Ctrl+K command palette ([#150](https://github.com/plutocms/pluto/issues/150)) ([540a4fb](https://github.com/plutocms/pluto/commit/540a4fb5f6fb5ed8b3d32fc05efe70f862222ff8))
|
|
30
|
+
* **admin:** add a content overview widget to the dashboard ([#152](https://github.com/plutocms/pluto/issues/152)) ([b925f83](https://github.com/plutocms/pluto/commit/b925f8383d64b64b47738bfad2617a8ecbeb3172))
|
|
31
|
+
* **admin:** make the view aside resizable ([#147](https://github.com/plutocms/pluto/issues/147)) ([f400cc1](https://github.com/plutocms/pluto/commit/f400cc1fa54fbcab7b4bc30c18d8f3cddad026d9))
|
|
32
|
+
* **admin:** redesign the dashboard, isolate the admin theme, and use one Tailwind build ([#153](https://github.com/plutocms/pluto/issues/153)) ([e1461b9](https://github.com/plutocms/pluto/commit/e1461b9a58267e71b42e2bceadc7c1f6d471861e))
|
|
33
|
+
* **content:** add loading, empty, and error states to the content list ([#151](https://github.com/plutocms/pluto/issues/151)) ([7d26540](https://github.com/plutocms/pluto/commit/7d2654027c4329b8fc08843a672b43d73a4e7e55))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
|
|
38
|
+
* **admin:** start the dashboard group below the navbar again ([#149](https://github.com/plutocms/pluto/issues/149)) ([32d4970](https://github.com/plutocms/pluto/commit/32d497013c487b317d376376e645a502f9054a35))
|
|
39
|
+
|
|
4
40
|
## [0.11.0](https://github.com/plutocms/pluto/compare/v0.10.0...v0.11.0) (2026-10-16)
|
|
5
41
|
|
|
6
42
|
|
package/FEATURES.md
CHANGED
|
@@ -23,3 +23,16 @@ policies shared by every project here.
|
|
|
23
23
|
`PlutoViewAside`), the generic content form's publish workflow, and the `contentActions`
|
|
24
24
|
registry bucket that lets a layer add its own buttons to that form.
|
|
25
25
|
@.claude/skills/admin-view-chrome/SKILL.md
|
|
26
|
+
- Command palette: Cmd/Ctrl+K and a sidebar search button open a `UDashboardSearch` with
|
|
27
|
+
"New <type>" commands for writable content types, every admin nav destination, and theme
|
|
28
|
+
commands. @.claude/skills/command-palette/SKILL.md
|
|
29
|
+
- Dashboard: the admin home with a date and title header, a create menu, and core widgets:
|
|
30
|
+
content tiles (total, +N this week, published/draft meter), a `UChart` weekly activity
|
|
31
|
+
chart backed by the optional adapter `countCreated`, and quick actions.
|
|
32
|
+
@.claude/skills/dashboard/SKILL.md
|
|
33
|
+
- Admin theme: the admin keeps its own palette, chart colors, surfaces, and font under
|
|
34
|
+
`data-pluto-admin`, so the consumer app's site theme does not reach it.
|
|
35
|
+
@.claude/skills/admin-theme/SKILL.md
|
|
36
|
+
- Single Tailwind build: the app owns the only Tailwind build. The pluto-css module generates
|
|
37
|
+
`#build/pluto.css` from each layer's `pluto-layer.css`, adds a fallback entry, and warns
|
|
38
|
+
about a wrong CSS setup. @.claude/skills/single-tailwind-build/SKILL.md
|
package/README.md
CHANGED
|
@@ -135,6 +135,36 @@ export default defineNuxtConfig({
|
|
|
135
135
|
})
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
### Styles: one Tailwind build
|
|
139
|
+
|
|
140
|
+
Your app owns the only Tailwind build. Pluto layers do not register their own. Start your
|
|
141
|
+
CSS entry with these three imports, then add your own theme after them:
|
|
142
|
+
|
|
143
|
+
```css
|
|
144
|
+
/* app/assets/css/main.css */
|
|
145
|
+
@import 'tailwindcss';
|
|
146
|
+
@import '@nuxt/ui';
|
|
147
|
+
@import '#build/pluto.css';
|
|
148
|
+
|
|
149
|
+
@theme {
|
|
150
|
+
/* your site's tokens */
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// nuxt.config.ts
|
|
156
|
+
export default defineNuxtConfig({
|
|
157
|
+
css: ['~/assets/css/main.css'],
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`#build/pluto.css` brings in the CSS of every Pluto layer, including the admin theme. Nuxt UI
|
|
162
|
+
already scans the source of every layer. If your app has no Tailwind entry, Pluto uses a
|
|
163
|
+
fallback entry. If the setup is wrong, the build prints a `pluto-css` warning with the fix.
|
|
164
|
+
|
|
165
|
+
The admin keeps its own palette and font. Your site's `app.config` colors and `@theme`
|
|
166
|
+
tokens do not change the admin.
|
|
167
|
+
|
|
138
168
|
## Dashboard & headless usage
|
|
139
169
|
|
|
140
170
|
Although Pluto is built on Nuxt, it is not limited to Nuxt-only consumption.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The Pluto admin theme.
|
|
3
|
+
*
|
|
4
|
+
* Two selectors carry it:
|
|
5
|
+
* - `:root[data-pluto-admin]`: usePlutoAdminTheme() sets this attribute on
|
|
6
|
+
* <html> while an admin layout is mounted, so the theme also covers
|
|
7
|
+
* overlays (modals, slideovers, menus, toasts, the command palette) that
|
|
8
|
+
* render at the end of <body>.
|
|
9
|
+
* - `[data-pluto-admin-scope]`: for admin chrome that also shows on the
|
|
10
|
+
* public site, such as the admin navbar for a logged-in admin.
|
|
11
|
+
*
|
|
12
|
+
* `[data-pluto-admin-dark]` takes the dark tokens in both color modes. The
|
|
13
|
+
* admin navbar is near black in light mode too, so its text and buttons need
|
|
14
|
+
* the dark values.
|
|
15
|
+
*
|
|
16
|
+
* Every value is a literal. The consumer app styles its public site through
|
|
17
|
+
* its own app.config colors and @theme. Both reach the admin unless the admin
|
|
18
|
+
* pins its own values: app.config colors set Nuxt UI's --ui-color-* scales on
|
|
19
|
+
* :root, and @theme can redefine a Tailwind color such as --color-red-400.
|
|
20
|
+
* These selectors are more specific than Nuxt UI's :root and .dark rules.
|
|
21
|
+
*
|
|
22
|
+
* The semantic tokens (--ui-primary, --ui-bg, --ui-text, ...) are declared
|
|
23
|
+
* again here, not only the color scales. A custom property inherits as a
|
|
24
|
+
* resolved value, so on a scoped element the :root tokens would still hold
|
|
25
|
+
* the app's colors.
|
|
26
|
+
*
|
|
27
|
+
* Chart colors are a validated categorical palette: lightness band, chroma
|
|
28
|
+
* floor, colorblind separation, and 3:1 contrast pass on white (light) and
|
|
29
|
+
* on zinc-900 (dark). This file is generated; keep the values in sync with
|
|
30
|
+
* scripts/generate-admin-theme.cjs: run it from the repo root, never edit this
|
|
31
|
+
* file by hand.
|
|
32
|
+
*/
|
|
33
|
+
:root[data-pluto-admin],
|
|
34
|
+
[data-pluto-admin-scope] {
|
|
35
|
+
/* primary: indigo */
|
|
36
|
+
--ui-color-primary-50: oklch(96.2% 0.018 272.314);
|
|
37
|
+
--ui-color-primary-100: oklch(93% 0.034 272.788);
|
|
38
|
+
--ui-color-primary-200: oklch(87% 0.065 274.039);
|
|
39
|
+
--ui-color-primary-300: oklch(78.5% 0.115 274.713);
|
|
40
|
+
--ui-color-primary-400: oklch(67.3% 0.182 276.935);
|
|
41
|
+
--ui-color-primary-500: oklch(58.5% 0.233 277.117);
|
|
42
|
+
--ui-color-primary-600: oklch(51.1% 0.262 276.966);
|
|
43
|
+
--ui-color-primary-700: oklch(45.7% 0.24 277.023);
|
|
44
|
+
--ui-color-primary-800: oklch(39.8% 0.195 277.366);
|
|
45
|
+
--ui-color-primary-900: oklch(35.9% 0.144 278.697);
|
|
46
|
+
--ui-color-primary-950: oklch(25.7% 0.09 281.288);
|
|
47
|
+
|
|
48
|
+
/* secondary: violet */
|
|
49
|
+
--ui-color-secondary-50: oklch(96.9% 0.016 293.756);
|
|
50
|
+
--ui-color-secondary-100: oklch(94.3% 0.029 294.588);
|
|
51
|
+
--ui-color-secondary-200: oklch(89.4% 0.057 293.283);
|
|
52
|
+
--ui-color-secondary-300: oklch(81.1% 0.111 293.571);
|
|
53
|
+
--ui-color-secondary-400: oklch(70.2% 0.183 293.541);
|
|
54
|
+
--ui-color-secondary-500: oklch(60.6% 0.25 292.717);
|
|
55
|
+
--ui-color-secondary-600: oklch(54.1% 0.281 293.009);
|
|
56
|
+
--ui-color-secondary-700: oklch(49.1% 0.27 292.581);
|
|
57
|
+
--ui-color-secondary-800: oklch(43.2% 0.232 292.759);
|
|
58
|
+
--ui-color-secondary-900: oklch(38% 0.189 293.745);
|
|
59
|
+
--ui-color-secondary-950: oklch(28.3% 0.141 291.089);
|
|
60
|
+
|
|
61
|
+
/* success: green */
|
|
62
|
+
--ui-color-success-50: oklch(98.2% 0.018 155.826);
|
|
63
|
+
--ui-color-success-100: oklch(96.2% 0.044 156.743);
|
|
64
|
+
--ui-color-success-200: oklch(92.5% 0.084 155.995);
|
|
65
|
+
--ui-color-success-300: oklch(87.1% 0.15 154.449);
|
|
66
|
+
--ui-color-success-400: oklch(79.2% 0.209 151.711);
|
|
67
|
+
--ui-color-success-500: oklch(72.3% 0.219 149.579);
|
|
68
|
+
--ui-color-success-600: oklch(62.7% 0.194 149.214);
|
|
69
|
+
--ui-color-success-700: oklch(52.7% 0.154 150.069);
|
|
70
|
+
--ui-color-success-800: oklch(44.8% 0.119 151.328);
|
|
71
|
+
--ui-color-success-900: oklch(39.3% 0.095 152.535);
|
|
72
|
+
--ui-color-success-950: oklch(26.6% 0.065 152.934);
|
|
73
|
+
|
|
74
|
+
/* info: blue */
|
|
75
|
+
--ui-color-info-50: oklch(97% 0.014 254.604);
|
|
76
|
+
--ui-color-info-100: oklch(93.2% 0.032 255.585);
|
|
77
|
+
--ui-color-info-200: oklch(88.2% 0.059 254.128);
|
|
78
|
+
--ui-color-info-300: oklch(80.9% 0.105 251.813);
|
|
79
|
+
--ui-color-info-400: oklch(70.7% 0.165 254.624);
|
|
80
|
+
--ui-color-info-500: oklch(62.3% 0.214 259.815);
|
|
81
|
+
--ui-color-info-600: oklch(54.6% 0.245 262.881);
|
|
82
|
+
--ui-color-info-700: oklch(48.8% 0.243 264.376);
|
|
83
|
+
--ui-color-info-800: oklch(42.4% 0.199 265.638);
|
|
84
|
+
--ui-color-info-900: oklch(37.9% 0.146 265.522);
|
|
85
|
+
--ui-color-info-950: oklch(28.2% 0.091 267.935);
|
|
86
|
+
|
|
87
|
+
/* warning: amber */
|
|
88
|
+
--ui-color-warning-50: oklch(98.7% 0.022 95.277);
|
|
89
|
+
--ui-color-warning-100: oklch(96.2% 0.059 95.617);
|
|
90
|
+
--ui-color-warning-200: oklch(92.4% 0.12 95.746);
|
|
91
|
+
--ui-color-warning-300: oklch(87.9% 0.169 91.605);
|
|
92
|
+
--ui-color-warning-400: oklch(82.8% 0.189 84.429);
|
|
93
|
+
--ui-color-warning-500: oklch(76.9% 0.188 70.08);
|
|
94
|
+
--ui-color-warning-600: oklch(66.6% 0.179 58.318);
|
|
95
|
+
--ui-color-warning-700: oklch(55.5% 0.163 48.998);
|
|
96
|
+
--ui-color-warning-800: oklch(47.3% 0.137 46.201);
|
|
97
|
+
--ui-color-warning-900: oklch(41.4% 0.112 45.904);
|
|
98
|
+
--ui-color-warning-950: oklch(27.9% 0.077 45.635);
|
|
99
|
+
|
|
100
|
+
/* error: red */
|
|
101
|
+
--ui-color-error-50: oklch(97.1% 0.013 17.38);
|
|
102
|
+
--ui-color-error-100: oklch(93.6% 0.032 17.717);
|
|
103
|
+
--ui-color-error-200: oklch(88.5% 0.062 18.334);
|
|
104
|
+
--ui-color-error-300: oklch(80.8% 0.114 19.571);
|
|
105
|
+
--ui-color-error-400: oklch(70.4% 0.191 22.216);
|
|
106
|
+
--ui-color-error-500: oklch(63.7% 0.237 25.331);
|
|
107
|
+
--ui-color-error-600: oklch(57.7% 0.245 27.325);
|
|
108
|
+
--ui-color-error-700: oklch(50.5% 0.213 27.518);
|
|
109
|
+
--ui-color-error-800: oklch(44.4% 0.177 26.899);
|
|
110
|
+
--ui-color-error-900: oklch(39.6% 0.141 25.723);
|
|
111
|
+
--ui-color-error-950: oklch(25.8% 0.092 26.042);
|
|
112
|
+
|
|
113
|
+
/* neutral: zinc */
|
|
114
|
+
--ui-color-neutral-50: oklch(98.5% 0 none);
|
|
115
|
+
--ui-color-neutral-100: oklch(96.7% 0.001 286.375);
|
|
116
|
+
--ui-color-neutral-200: oklch(92% 0.004 286.32);
|
|
117
|
+
--ui-color-neutral-300: oklch(87.1% 0.006 286.286);
|
|
118
|
+
--ui-color-neutral-400: oklch(70.5% 0.015 286.067);
|
|
119
|
+
--ui-color-neutral-500: oklch(55.2% 0.016 285.938);
|
|
120
|
+
--ui-color-neutral-600: oklch(44.2% 0.017 285.786);
|
|
121
|
+
--ui-color-neutral-700: oklch(37% 0.013 285.805);
|
|
122
|
+
--ui-color-neutral-800: oklch(27.4% 0.006 286.033);
|
|
123
|
+
--ui-color-neutral-900: oklch(21% 0.006 285.885);
|
|
124
|
+
--ui-color-neutral-950: oklch(14.1% 0.005 285.823);
|
|
125
|
+
|
|
126
|
+
--ui-primary: var(--ui-color-primary-500);
|
|
127
|
+
--ui-secondary: var(--ui-color-secondary-500);
|
|
128
|
+
--ui-success: var(--ui-color-success-500);
|
|
129
|
+
--ui-info: var(--ui-color-info-500);
|
|
130
|
+
--ui-warning: var(--ui-color-warning-500);
|
|
131
|
+
--ui-error: var(--ui-color-error-500);
|
|
132
|
+
|
|
133
|
+
--ui-text-dimmed: var(--ui-color-neutral-400);
|
|
134
|
+
--ui-text-muted: var(--ui-color-neutral-500);
|
|
135
|
+
--ui-text-toned: var(--ui-color-neutral-600);
|
|
136
|
+
--ui-text: var(--ui-color-neutral-700);
|
|
137
|
+
--ui-text-highlighted: var(--ui-color-neutral-900);
|
|
138
|
+
--ui-text-inverted: #fff;
|
|
139
|
+
--ui-bg: #fff;
|
|
140
|
+
--ui-bg-muted: var(--ui-color-neutral-50);
|
|
141
|
+
--ui-bg-elevated: var(--ui-color-neutral-100);
|
|
142
|
+
--ui-bg-accented: var(--ui-color-neutral-200);
|
|
143
|
+
--ui-bg-inverted: var(--ui-color-neutral-900);
|
|
144
|
+
--ui-border: var(--ui-color-neutral-200);
|
|
145
|
+
--ui-border-muted: var(--ui-color-neutral-200);
|
|
146
|
+
--ui-border-accented: var(--ui-color-neutral-300);
|
|
147
|
+
--ui-border-inverted: var(--ui-color-neutral-900);
|
|
148
|
+
|
|
149
|
+
--ui-chart-1: var(--ui-primary);
|
|
150
|
+
--ui-chart-2: oklch(66.6% 0.179 58.318); /* amber-600 */
|
|
151
|
+
--ui-chart-3: oklch(60% 0.118 184.704); /* teal-600 */
|
|
152
|
+
--ui-chart-4: oklch(64.5% 0.246 16.439); /* rose-500 */
|
|
153
|
+
--ui-chart-5: oklch(58.8% 0.158 241.966); /* sky-600 */
|
|
154
|
+
--ui-chart-6: oklch(53.2% 0.157 131.589); /* lime-700 */
|
|
155
|
+
|
|
156
|
+
/* Surfaces. The navbar is near black in both modes. */
|
|
157
|
+
--pluto-admin-canvas: #fff;
|
|
158
|
+
--pluto-admin-sidebar: oklch(98.5% 0 none);
|
|
159
|
+
--pluto-admin-navbar: oklch(14.1% 0.005 285.823);
|
|
160
|
+
|
|
161
|
+
--ui-radius: 0.375rem;
|
|
162
|
+
--font-sans: "Outfit", ui-sans-serif, system-ui, sans-serif;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
:root[data-pluto-admin].dark,
|
|
166
|
+
.dark [data-pluto-admin-scope],
|
|
167
|
+
[data-pluto-admin-dark] {
|
|
168
|
+
--ui-primary: var(--ui-color-primary-400);
|
|
169
|
+
--ui-secondary: var(--ui-color-secondary-400);
|
|
170
|
+
--ui-success: var(--ui-color-success-400);
|
|
171
|
+
--ui-info: var(--ui-color-info-400);
|
|
172
|
+
--ui-warning: var(--ui-color-warning-400);
|
|
173
|
+
--ui-error: var(--ui-color-error-400);
|
|
174
|
+
|
|
175
|
+
--ui-text-dimmed: var(--ui-color-neutral-500);
|
|
176
|
+
--ui-text-muted: var(--ui-color-neutral-400);
|
|
177
|
+
--ui-text-toned: var(--ui-color-neutral-300);
|
|
178
|
+
--ui-text: var(--ui-color-neutral-200);
|
|
179
|
+
--ui-text-highlighted: #fff;
|
|
180
|
+
--ui-text-inverted: var(--ui-color-neutral-900);
|
|
181
|
+
--ui-bg: oklch(24% 0.006 286);
|
|
182
|
+
--ui-bg-muted: var(--ui-color-neutral-800);
|
|
183
|
+
--ui-bg-elevated: var(--ui-color-neutral-800);
|
|
184
|
+
--ui-bg-accented: var(--ui-color-neutral-700);
|
|
185
|
+
--ui-bg-inverted: #fff;
|
|
186
|
+
--ui-border: var(--ui-color-neutral-800);
|
|
187
|
+
--ui-border-muted: var(--ui-color-neutral-700);
|
|
188
|
+
--ui-border-accented: var(--ui-color-neutral-700);
|
|
189
|
+
--ui-border-inverted: #fff;
|
|
190
|
+
|
|
191
|
+
--ui-chart-1: var(--ui-primary);
|
|
192
|
+
--ui-chart-2: oklch(66.6% 0.179 58.318); /* amber-600 */
|
|
193
|
+
--ui-chart-3: oklch(60% 0.118 184.704); /* teal-600 */
|
|
194
|
+
--ui-chart-4: oklch(64.5% 0.246 16.439); /* rose-500 */
|
|
195
|
+
--ui-chart-5: oklch(58.8% 0.158 241.966); /* sky-600 */
|
|
196
|
+
--ui-chart-6: oklch(64.8% 0.2 131.684); /* lime-600 */
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* Four steps, darkest at the top: a near-black navbar, a darker sidebar,
|
|
200
|
+
* a dim dark gray canvas (not pitch black), and cards (--ui-bg) one step
|
|
201
|
+
* above the canvas.
|
|
202
|
+
*/
|
|
203
|
+
--pluto-admin-navbar: oklch(14.1% 0.005 285.823);
|
|
204
|
+
--pluto-admin-sidebar: oklch(17.5% 0.0055 286);
|
|
205
|
+
--pluto-admin-canvas: oklch(21% 0.006 285.885);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
:root[data-pluto-admin] body {
|
|
209
|
+
background-color: var(--pluto-admin-canvas);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
:root[data-pluto-admin] body,
|
|
213
|
+
[data-pluto-admin-scope] {
|
|
214
|
+
font-family: var(--font-sans);
|
|
215
|
+
color: var(--ui-text);
|
|
216
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The pluto layer's CSS. It is not a Tailwind entry.
|
|
3
|
+
*
|
|
4
|
+
* The app's CSS entry owns the one Tailwind build of the app:
|
|
5
|
+
*
|
|
6
|
+
* @import 'tailwindcss';
|
|
7
|
+
* @import '@nuxt/ui';
|
|
8
|
+
* @import '#build/pluto.css';
|
|
9
|
+
*
|
|
10
|
+
* `#build/pluto.css` imports this file (and the same file of every other
|
|
11
|
+
* Pluto layer). Rules for this file:
|
|
12
|
+
* - Do not import tailwindcss or @nuxt/ui here.
|
|
13
|
+
* - Use only @theme, @custom-variant, @utility, unlayered rules, and
|
|
14
|
+
* imports of plain CSS files next to this one.
|
|
15
|
+
* - Add no @source for app/: Nuxt UI already scans every layer's app/ dir.
|
|
16
|
+
* See the single-tailwind-build skill.
|
|
17
|
+
*/
|
|
18
|
+
@import './admin-theme.css';
|
|
19
|
+
|
|
20
|
+
@theme {
|
|
21
|
+
--color-admin-content: var(--color-zinc-800);
|
|
22
|
+
--color-admin-sidebar: var(--color-zinc-900);
|
|
23
|
+
|
|
24
|
+
--font-outfit: 'Outfit', sans-serif;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Set the header height outside a layer. This overrides Nuxt UI's own
|
|
29
|
+
* `@layer theme` value for the same variable. The values match the
|
|
30
|
+
* navbar's own height classes (`h-14` / `lg:h-10`).
|
|
31
|
+
*/
|
|
32
|
+
:root {
|
|
33
|
+
--ui-header-height: 3.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media (width >= 64rem) {
|
|
37
|
+
:root {
|
|
38
|
+
--ui-header-height: 2.5rem;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -15,9 +15,11 @@ const isDark = computed({
|
|
|
15
15
|
<ClientOnly v-if="!colorMode?.forced">
|
|
16
16
|
<UButton
|
|
17
17
|
:icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
|
|
18
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
18
19
|
color="neutral"
|
|
19
|
-
variant="
|
|
20
|
-
size="
|
|
20
|
+
variant="ghost"
|
|
21
|
+
size="md"
|
|
22
|
+
square
|
|
21
23
|
@click="isDark = !isDark"
|
|
22
24
|
/>
|
|
23
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { TableColumn } from '@nuxt/ui'
|
|
2
|
+
import type { ButtonProps, TableColumn } from '@nuxt/ui'
|
|
3
3
|
|
|
4
4
|
const props = defineProps<{
|
|
5
5
|
/** A content type's `name` (not a registry entry id) — see `usePlutoContentTypes`. */
|
|
@@ -48,7 +48,62 @@ function editPath(id: string | number) {
|
|
|
48
48
|
return contentType.value?.editPath?.(id) ?? `${basePath.value}/${id}`
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Load the list 50 rows at a time. The table is virtualized, so only the
|
|
52
|
+
// visible rows render, and the next page loads when the user scrolls near
|
|
53
|
+
// the end of the table (or of the card list, on mobile).
|
|
54
|
+
const PAGE_SIZE = 50
|
|
55
|
+
const { items, total, pending, loadingMore, hasMore, error, refresh, loadMore, remove } = usePlutoContentList(
|
|
56
|
+
props.type,
|
|
57
|
+
{ pageSize: PAGE_SIZE }
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const tableRef = useTemplateRef<{
|
|
61
|
+
$el: HTMLElement
|
|
62
|
+
}>('table')
|
|
63
|
+
const mobileListRef = useTemplateRef<HTMLElement>('mobileList')
|
|
64
|
+
|
|
65
|
+
function loadMoreSafely() {
|
|
66
|
+
// A failure lands in `error`, and the alert offers "Try again".
|
|
67
|
+
loadMore().catch(() => {})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Stop while an error shows, so a failing page does not load in a loop.
|
|
71
|
+
const canLoadMore = () => hasMore.value && !error.value
|
|
72
|
+
useInfiniteScroll(() => tableRef.value?.$el, loadMoreSafely, { distance: 240, canLoadMore })
|
|
73
|
+
useInfiniteScroll(mobileListRef, loadMoreSafely, { distance: 240, canLoadMore })
|
|
74
|
+
|
|
75
|
+
const number = new Intl.NumberFormat()
|
|
76
|
+
|
|
77
|
+
// What to show while the list has no rows. With rows, the list shows
|
|
78
|
+
// them, and a failed refresh shows an alert above them instead.
|
|
79
|
+
const emptyState = computed<'loading' | 'error' | 'empty' | null>(() => {
|
|
80
|
+
if (items.value.length > 0) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
if (pending.value) {
|
|
84
|
+
return 'loading'
|
|
85
|
+
}
|
|
86
|
+
return error.value ? 'error' : 'empty'
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
function retry() {
|
|
90
|
+
refresh().catch(() => {})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Buttons navigate with `navigateTo`, not `to`: `to` is typed per app
|
|
94
|
+
// through typed pages, so a computed button list cannot type it here.
|
|
95
|
+
const emptyActions = computed<ButtonProps[]>(() => {
|
|
96
|
+
const type = contentType.value
|
|
97
|
+
if (!type) {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
if (emptyState.value === 'error') {
|
|
101
|
+
return [{ label: 'Try again', icon: 'lucide:refresh-ccw', color: 'neutral', variant: 'outline', onClick: retry }]
|
|
102
|
+
}
|
|
103
|
+
return canWrite.value
|
|
104
|
+
? [{ label: `New ${type.label}`, icon: 'lucide:plus', onClick: () => navigateTo(newPath.value) }]
|
|
105
|
+
: []
|
|
106
|
+
})
|
|
52
107
|
|
|
53
108
|
const listFields = computed(() =>
|
|
54
109
|
(contentType.value?.fields ?? [])
|
|
@@ -88,7 +143,7 @@ async function confirmDelete() {
|
|
|
88
143
|
</script>
|
|
89
144
|
|
|
90
145
|
<template>
|
|
91
|
-
<PlutoViewAdminPanel v-if="contentType">
|
|
146
|
+
<PlutoViewAdminPanel v-if="contentType" fill>
|
|
92
147
|
<template #toolbar>
|
|
93
148
|
<PlutoViewToolbar :title="contentType.labelPlural">
|
|
94
149
|
<template #trailing>
|
|
@@ -98,20 +153,59 @@ async function confirmDelete() {
|
|
|
98
153
|
variant="ghost"
|
|
99
154
|
title="Refresh"
|
|
100
155
|
square
|
|
101
|
-
@click="
|
|
156
|
+
@click="retry"
|
|
102
157
|
/>
|
|
103
158
|
</template>
|
|
104
159
|
|
|
105
160
|
<template v-if="canWrite" #right>
|
|
106
161
|
<UButton :to="newPath" icon="lucide:plus" as="NuxtLink">
|
|
107
|
-
|
|
162
|
+
New {{ contentType.label }}
|
|
108
163
|
</UButton>
|
|
109
164
|
</template>
|
|
110
165
|
</PlutoViewToolbar>
|
|
111
166
|
</template>
|
|
112
167
|
|
|
113
|
-
<
|
|
114
|
-
|
|
168
|
+
<UEmpty
|
|
169
|
+
v-if="emptyState === 'loading'"
|
|
170
|
+
:title="`Loading ${contentType.labelPlural.toLowerCase()}…`"
|
|
171
|
+
variant="naked"
|
|
172
|
+
loading
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<UEmpty
|
|
176
|
+
v-else-if="emptyState === 'error'"
|
|
177
|
+
:title="`Could not load ${contentType.labelPlural.toLowerCase()}`"
|
|
178
|
+
:actions="emptyActions"
|
|
179
|
+
description="Check your connection and try again."
|
|
180
|
+
icon="lucide:circle-alert"
|
|
181
|
+
variant="naked"
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<UEmpty
|
|
185
|
+
v-else-if="emptyState === 'empty'"
|
|
186
|
+
:title="`No ${contentType.labelPlural.toLowerCase()} yet`"
|
|
187
|
+
:description="canWrite ? `Create the first ${contentType.label.toLowerCase()} to see it here.` : 'There is nothing here yet.'"
|
|
188
|
+
:icon="contentType.icon ?? 'lucide:inbox'"
|
|
189
|
+
:actions="emptyActions"
|
|
190
|
+
variant="naked"
|
|
191
|
+
/>
|
|
192
|
+
|
|
193
|
+
<UAlert
|
|
194
|
+
v-if="!emptyState && error"
|
|
195
|
+
:actions="[{ label: 'Try again', color: 'error', variant: 'outline', onClick: retry }]"
|
|
196
|
+
:title="`Could not refresh ${contentType.labelPlural.toLowerCase()}`"
|
|
197
|
+
description="The list below may be out of date."
|
|
198
|
+
color="error"
|
|
199
|
+
variant="subtle"
|
|
200
|
+
icon="lucide:circle-alert"
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
<div
|
|
204
|
+
v-if="!emptyState"
|
|
205
|
+
ref="mobileList"
|
|
206
|
+
class="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto lg:hidden"
|
|
207
|
+
>
|
|
208
|
+
<UCard v-for="item in items" :key="item.id" class="shrink-0">
|
|
115
209
|
<div class="flex flex-col gap-4">
|
|
116
210
|
<div class="min-w-0">
|
|
117
211
|
<NuxtLink
|
|
@@ -156,15 +250,18 @@ async function confirmDelete() {
|
|
|
156
250
|
</UCard>
|
|
157
251
|
</div>
|
|
158
252
|
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
253
|
+
<UTable
|
|
254
|
+
v-if="!emptyState"
|
|
255
|
+
ref="table"
|
|
256
|
+
:data="items"
|
|
257
|
+
:columns="columns"
|
|
258
|
+
:loading="pending || loadingMore"
|
|
259
|
+
class="ring-default hidden min-h-0 flex-1 rounded-lg ring lg:block"
|
|
260
|
+
empty="No items yet."
|
|
261
|
+
sticky
|
|
262
|
+
virtualize
|
|
263
|
+
>
|
|
264
|
+
<template #actions-cell="{ row }">
|
|
168
265
|
<div class="flex justify-end gap-3">
|
|
169
266
|
<NuxtLink
|
|
170
267
|
v-if="canWrite"
|
|
@@ -183,10 +280,15 @@ async function confirmDelete() {
|
|
|
183
280
|
Remove
|
|
184
281
|
</button>
|
|
185
282
|
</div>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
283
|
+
</template>
|
|
284
|
+
</UTable>
|
|
285
|
+
|
|
286
|
+
<p v-if="!emptyState" class="text-muted shrink-0 text-xs tabular-nums" aria-live="polite">
|
|
287
|
+
Showing {{ number.format(items.length) }}
|
|
288
|
+
<template v-if="total !== undefined">of {{ number.format(total) }}</template>
|
|
289
|
+
{{ contentType.labelPlural.toLowerCase() }}
|
|
290
|
+
<template v-if="loadingMore">· Loading more…</template>
|
|
291
|
+
</p>
|
|
190
292
|
</PlutoViewAdminPanel>
|
|
191
293
|
|
|
192
294
|
<PlutoViewAdminPanel v-else>
|