@nuxt-customer-portal/ui 0.10.1 → 0.10.3
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 +17 -0
- package/README.md +30 -0
- package/app/app.config.ts +114 -5
- package/app/assets/css/surfaces.css +38 -0
- package/app/components/PortalMarkdownEditor.vue +9 -2
- package/app/components/PortalSidebarNavigation.vue +5 -1
- package/nuxt.config.ts +3 -0
- package/package.json +2 -2
- package/portal.manifest.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @nuxt-customer-portal/ui
|
|
2
2
|
|
|
3
|
+
## 0.10.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0e54ff2: Make link buttons easier to recognize with full-opacity colors, subtle hover backgrounds, and stronger pressed backgrounds without adding underlines. Preserve native keyboard focus indicators, disabled states, control padding, and rounded corners.
|
|
8
|
+
- @nuxt-customer-portal/core@0.10.3
|
|
9
|
+
|
|
10
|
+
## 0.10.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- c2985a7: Limit Email Texts to enabled portal languages and hide the selector for single-language portals. Use the configured control surface for subject and test-recipient fields, neutral actions, Markdown placeholder selectors and their menus, and link fields. Remove the duplicate Email Texts card heading.
|
|
15
|
+
- c2985a7: Restrict legal page language selection to enabled portal languages and hide the selector for single-language portals. Use the configured surface and text colors for legal title inputs and Markdown editors, including source mode.
|
|
16
|
+
- c2985a7: Centralize portal surface styling so cards, inputs, rich-text editors, dialogs, and menus use the configured surface in both light and dark mode. Use Nuxt UI theme overrides and Tailwind rounding utilities while preserving native size variants and input padding. Keep neutral outline actions transparent and preserve branded selection states and the Brutal theme. Remove page-specific input background overrides and simplify nested Products form sections.
|
|
17
|
+
- Updated dependencies [c2985a7]
|
|
18
|
+
- @nuxt-customer-portal/core@0.10.2
|
|
19
|
+
|
|
3
20
|
## 0.10.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -4,6 +4,36 @@ Neutral shell primitives and fallback layouts for Nuxt Customer Portal. Hosts
|
|
|
4
4
|
can override every layout and branded component while retaining the shared
|
|
5
5
|
feature registry, session state, and navigation contracts from core.
|
|
6
6
|
|
|
7
|
+
## Shared visual styling
|
|
8
|
+
|
|
9
|
+
The portal separates the page canvas (`--ui-bg`) from content surfaces
|
|
10
|
+
(`--portal-surface`). Cards, dialogs, menus, text inputs, selects, and Markdown
|
|
11
|
+
editors use the same opaque surface. The default is white in light mode and
|
|
12
|
+
slate in dark mode; saved appearance settings override these colors without
|
|
13
|
+
changing each form. Products and other feature packages inherit these defaults.
|
|
14
|
+
|
|
15
|
+
Use standard Nuxt UI components instead of setting local input backgrounds.
|
|
16
|
+
Neutral outline buttons remain transparent, primary actions use the brand
|
|
17
|
+
color, and selected items use a soft brand tint. Reserve explicit backgrounds
|
|
18
|
+
for meaningful states, not for distinguishing one input type from another.
|
|
19
|
+
Sidebar submenu hover uses the same 10% brand tint and brand-colored text as
|
|
20
|
+
top-level primary ghost links, such as Clients. Active submenu links keep that
|
|
21
|
+
tint with a stronger font weight, and keyboard focus retains a visible outline.
|
|
22
|
+
Group nested form sections with spacing and dividers rather than tinted cards.
|
|
23
|
+
|
|
24
|
+
Shared tokens in `app/assets/css/surfaces.css` define surface text, borders,
|
|
25
|
+
and hover colors. Nuxt UI theme overrides use Tailwind rounding utilities:
|
|
26
|
+
normal controls and buttons have 8px corners and panels have 12px corners.
|
|
27
|
+
Nuxt UI's size variants, internal padding, and icon spacing remain unchanged,
|
|
28
|
+
including compact toolbar controls. The Brutal theme retains its square
|
|
29
|
+
corners, heavier rules, and distinct typography.
|
|
30
|
+
|
|
31
|
+
`portal-surface` establishes surface-specific text and border colors for a
|
|
32
|
+
container, including teleported menus and dialogs. `portal-field-surface` does
|
|
33
|
+
the same for a field without changing its parent or the page canvas. Do not
|
|
34
|
+
reassign global `--ui-bg` to the surface color: page backgrounds and nested
|
|
35
|
+
surface colors intentionally serve different roles.
|
|
36
|
+
|
|
7
37
|
`PortalMarkdownEditor` provides visual formatting and a raw Markdown mode over
|
|
8
38
|
one string `v-model`. It integrates with Nuxt UI `UFormField` validation and
|
|
9
39
|
inherits the form's disabled state. Use it for fields rendered as Markdown;
|
package/app/app.config.ts
CHANGED
|
@@ -1,31 +1,140 @@
|
|
|
1
|
-
const
|
|
1
|
+
const surface = 'portal-surface bg-(--portal-surface) text-(--portal-surface-text)'
|
|
2
|
+
const dropdownContent = `${surface} rounded-lg ring-1 ring-default shadow-md`
|
|
2
3
|
const selectItem =
|
|
3
|
-
'data-highlighted:not-data-disabled:before:bg-
|
|
4
|
+
'data-highlighted:not-data-disabled:before:bg-elevated data-[state=checked]:before:bg-primary/10 data-[state=checked]:text-primary data-[state=checked]:font-medium'
|
|
5
|
+
const linkInteraction =
|
|
6
|
+
'hover:disabled:bg-transparent hover:aria-disabled:bg-transparent active:disabled:bg-transparent active:aria-disabled:bg-transparent'
|
|
7
|
+
const linkColors = {
|
|
8
|
+
primary: 'hover:text-primary active:text-primary hover:bg-primary/10 active:bg-primary/20',
|
|
9
|
+
secondary: 'hover:text-secondary active:text-secondary hover:bg-secondary/10 active:bg-secondary/20',
|
|
10
|
+
success: 'hover:text-success active:text-success hover:bg-success/10 active:bg-success/20',
|
|
11
|
+
info: 'hover:text-info active:text-info hover:bg-info/10 active:bg-info/20',
|
|
12
|
+
warning: 'hover:text-warning active:text-warning hover:bg-warning/10 active:bg-warning/20',
|
|
13
|
+
error: 'hover:text-error active:text-error hover:bg-error/10 active:bg-error/20',
|
|
14
|
+
neutral:
|
|
15
|
+
'hover:text-highlighted active:text-highlighted hover:bg-(--portal-surface-hover) active:bg-(--portal-surface-active)'
|
|
16
|
+
}
|
|
17
|
+
const field = {
|
|
18
|
+
slots: { base: 'rounded-lg' },
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
outline: 'portal-field-surface bg-(--portal-surface) text-highlighted ring ring-inset ring-accented',
|
|
22
|
+
soft: 'portal-field-surface bg-(--portal-surface) text-highlighted ring ring-inset ring-accented',
|
|
23
|
+
subtle: 'portal-field-surface bg-(--portal-surface) text-highlighted ring ring-inset ring-accented'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const panelVariants = {
|
|
28
|
+
outline: { root: `${surface} ring ring-default` },
|
|
29
|
+
soft: { root: `${surface} ring ring-default` },
|
|
30
|
+
subtle: { root: `${surface} ring ring-default` }
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
export default defineAppConfig({
|
|
6
34
|
portal: { sidebarNavigation: false },
|
|
7
35
|
ui: {
|
|
8
36
|
button: {
|
|
37
|
+
slots: { base: 'rounded-lg' },
|
|
9
38
|
compoundVariants: [
|
|
10
39
|
{
|
|
11
40
|
color: 'primary',
|
|
12
41
|
variant: 'solid',
|
|
13
42
|
class: 'text-[color:var(--portal-on-primary,var(--ui-text-inverted))] hover:bg-primary active:bg-primary'
|
|
14
|
-
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
color: 'neutral',
|
|
46
|
+
variant: 'outline',
|
|
47
|
+
class:
|
|
48
|
+
'bg-transparent hover:bg-(--portal-surface-hover) active:bg-(--portal-surface-active) disabled:bg-transparent aria-disabled:bg-transparent'
|
|
49
|
+
},
|
|
50
|
+
...Object.entries(linkColors).map(([color, states]) => ({
|
|
51
|
+
color,
|
|
52
|
+
variant: 'link',
|
|
53
|
+
class: `${linkInteraction} ${states}`
|
|
54
|
+
}))
|
|
15
55
|
]
|
|
16
56
|
},
|
|
57
|
+
card: {
|
|
58
|
+
slots: { root: 'rounded-xl shadow-none' },
|
|
59
|
+
variants: { variant: panelVariants }
|
|
60
|
+
},
|
|
61
|
+
pageCard: {
|
|
62
|
+
slots: { root: 'rounded-xl shadow-none' },
|
|
63
|
+
variants: { variant: panelVariants }
|
|
64
|
+
},
|
|
65
|
+
input: field,
|
|
66
|
+
inputNumber: field,
|
|
67
|
+
inputDate: field,
|
|
68
|
+
inputTime: field,
|
|
69
|
+
inputTags: field,
|
|
70
|
+
textarea: field,
|
|
71
|
+
header: {
|
|
72
|
+
slots: { root: surface, content: surface }
|
|
73
|
+
},
|
|
74
|
+
inputMenu: {
|
|
75
|
+
...field,
|
|
76
|
+
slots: { ...field.slots, content: dropdownContent, item: selectItem }
|
|
77
|
+
},
|
|
17
78
|
select: {
|
|
18
|
-
|
|
79
|
+
...field,
|
|
80
|
+
slots: { ...field.slots, content: dropdownContent, item: selectItem, arrow: 'fill-(--portal-surface)' }
|
|
19
81
|
},
|
|
20
82
|
selectMenu: {
|
|
21
|
-
|
|
83
|
+
...field,
|
|
84
|
+
slots: { ...field.slots, content: dropdownContent, item: selectItem, arrow: 'fill-(--portal-surface)' }
|
|
85
|
+
},
|
|
86
|
+
modal: {
|
|
87
|
+
slots: { content: `${surface} rounded-xl` }
|
|
88
|
+
},
|
|
89
|
+
slideover: {
|
|
90
|
+
slots: { content: surface }
|
|
91
|
+
},
|
|
92
|
+
popover: {
|
|
93
|
+
slots: { content: dropdownContent, arrow: 'fill-(--portal-surface)' }
|
|
22
94
|
},
|
|
95
|
+
toast: {
|
|
96
|
+
slots: { root: surface }
|
|
97
|
+
},
|
|
98
|
+
editorEmojiMenu: { slots: { content: dropdownContent } },
|
|
99
|
+
editorMentionMenu: { slots: { content: dropdownContent } },
|
|
100
|
+
editorSuggestionMenu: { slots: { content: dropdownContent } },
|
|
23
101
|
dropdownMenu: {
|
|
24
102
|
slots: {
|
|
25
103
|
content: dropdownContent,
|
|
26
104
|
item: 'data-highlighted:before:bg-accented data-[state=open]:before:bg-accented'
|
|
27
105
|
}
|
|
28
106
|
},
|
|
107
|
+
navigationMenu: {
|
|
108
|
+
slots: {
|
|
109
|
+
viewport: dropdownContent,
|
|
110
|
+
arrow: 'bg-(--portal-surface)',
|
|
111
|
+
link: 'focus-visible:before:outline-primary',
|
|
112
|
+
childLink: 'focus-visible:before:outline-primary'
|
|
113
|
+
},
|
|
114
|
+
compoundVariants: [
|
|
115
|
+
{
|
|
116
|
+
color: 'primary',
|
|
117
|
+
active: false,
|
|
118
|
+
disabled: false,
|
|
119
|
+
variant: 'pill',
|
|
120
|
+
class: {
|
|
121
|
+
link: 'hover:before:bg-primary/10 hover:text-primary',
|
|
122
|
+
linkLeadingIcon: 'group-hover:text-primary',
|
|
123
|
+
childLink: 'hover:before:bg-primary/10 hover:text-primary',
|
|
124
|
+
childLinkIcon: 'group-hover:text-primary'
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
color: 'primary',
|
|
129
|
+
active: true,
|
|
130
|
+
variant: 'pill',
|
|
131
|
+
class: {
|
|
132
|
+
link: 'before:bg-primary/10 text-primary font-semibold',
|
|
133
|
+
childLink: 'before:bg-primary/10 text-primary font-semibold'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
29
138
|
tabs: {
|
|
30
139
|
slots: {
|
|
31
140
|
trigger: 'hover:bg-elevated'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
:where(:root, .portal-appearance-preview) {
|
|
2
|
+
--portal-surface: #ffffff;
|
|
3
|
+
--portal-surface-ink: #0f172a;
|
|
4
|
+
--portal-control-radius: 0.5rem;
|
|
5
|
+
--portal-panel-radius: 0.75rem;
|
|
6
|
+
--portal-surface-text: color-mix(in srgb, var(--portal-surface-ink) 85%, var(--portal-surface));
|
|
7
|
+
--portal-surface-muted: color-mix(in srgb, var(--portal-surface-ink) 65%, var(--portal-surface));
|
|
8
|
+
--portal-surface-border: color-mix(in srgb, var(--portal-surface) 80%, var(--portal-surface-ink));
|
|
9
|
+
--portal-control-border: color-mix(in srgb, var(--portal-surface) 55%, var(--portal-surface-ink));
|
|
10
|
+
--portal-surface-hover: color-mix(in srgb, var(--portal-surface) 94%, var(--portal-surface-ink));
|
|
11
|
+
--portal-surface-active: color-mix(in srgb, var(--portal-surface) 88%, var(--portal-surface-ink));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:where(:root.dark, .portal-appearance-preview.dark) {
|
|
15
|
+
--portal-surface: #1e293b;
|
|
16
|
+
--portal-surface-ink: #f8fafc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.portal-surface,
|
|
20
|
+
.portal-field-surface {
|
|
21
|
+
--ui-bg: var(--portal-surface);
|
|
22
|
+
--ui-bg-muted: var(--portal-surface-hover);
|
|
23
|
+
--ui-bg-elevated: var(--portal-surface-hover);
|
|
24
|
+
--ui-bg-accented: var(--portal-surface-active);
|
|
25
|
+
--ui-text: var(--portal-surface-text);
|
|
26
|
+
--ui-text-highlighted: var(--portal-surface-ink);
|
|
27
|
+
--ui-text-toned: var(--portal-surface-text);
|
|
28
|
+
--ui-text-muted: var(--portal-surface-muted);
|
|
29
|
+
--ui-text-dimmed: var(--portal-surface-muted);
|
|
30
|
+
--ui-border: var(--portal-surface-border);
|
|
31
|
+
--ui-border-muted: var(--portal-surface-border);
|
|
32
|
+
--ui-border-accented: var(--portal-control-border);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.portal-markdown-editor {
|
|
36
|
+
background: var(--portal-surface);
|
|
37
|
+
color: var(--portal-surface-text);
|
|
38
|
+
}
|
|
@@ -230,7 +230,14 @@ onBeforeUnmount(() => editor.value?.destroy())
|
|
|
230
230
|
</script>
|
|
231
231
|
|
|
232
232
|
<template>
|
|
233
|
-
<div
|
|
233
|
+
<div
|
|
234
|
+
class="portal-markdown-editor portal-field-surface w-full rounded-lg border overflow-hidden focus-within:ring-2"
|
|
235
|
+
:class="
|
|
236
|
+
color === 'error'
|
|
237
|
+
? 'border-error focus-within:ring-error'
|
|
238
|
+
: 'border-(--portal-control-border) focus-within:ring-primary'
|
|
239
|
+
"
|
|
240
|
+
>
|
|
234
241
|
<div class="flex flex-wrap items-center gap-2 border-b border-default p-2">
|
|
235
242
|
<UButton
|
|
236
243
|
type="button"
|
|
@@ -269,7 +276,7 @@ onBeforeUnmount(() => editor.value?.destroy())
|
|
|
269
276
|
:disabled="disabled"
|
|
270
277
|
:placeholder="placeholder"
|
|
271
278
|
class="w-full font-mono"
|
|
272
|
-
:ui="{ base: 'rounded-none ring-0' }"
|
|
279
|
+
:ui="{ base: 'rounded-none ring-0 bg-inherit text-inherit' }"
|
|
273
280
|
@update:model-value="update(String($event))"
|
|
274
281
|
@focus="emitFormFocus"
|
|
275
282
|
@blur="emitFormBlur"
|
|
@@ -10,6 +10,7 @@ const { links } = useNavigationLinks(mobileOpen)
|
|
|
10
10
|
const expanded = ref<string[]>([])
|
|
11
11
|
const flyout = ref('')
|
|
12
12
|
const panel = ref<HTMLElement[]>([])
|
|
13
|
+
const mainMenuButtonUi = { base: 'hover:bg-primary/10 hover:text-primary active:bg-primary/10' }
|
|
13
14
|
let returnFocus: HTMLElement | undefined
|
|
14
15
|
let leaveTimer: ReturnType<typeof setTimeout> | undefined
|
|
15
16
|
|
|
@@ -98,7 +99,6 @@ const badgeProps = (badge: (typeof moduleNavigationGroups.value)[number]['badge'
|
|
|
98
99
|
collisionPadding: 12,
|
|
99
100
|
onOpenAutoFocus: (event: Event) => event.preventDefault()
|
|
100
101
|
}"
|
|
101
|
-
:ui="{ content: 'bg-default ring-1 ring-default shadow-lg' }"
|
|
102
102
|
@update:open="flyout = $event ? module.id : ''"
|
|
103
103
|
>
|
|
104
104
|
<template #anchor>
|
|
@@ -109,6 +109,7 @@ const badgeProps = (badge: (typeof moduleNavigationGroups.value)[number]['badge'
|
|
|
109
109
|
:aria-expanded="flyout === module.id"
|
|
110
110
|
:color="module.id === activeModuleId ? 'primary' : 'neutral'"
|
|
111
111
|
variant="ghost"
|
|
112
|
+
:ui="mainMenuButtonUi"
|
|
112
113
|
class="min-h-11 w-full justify-center rounded-md"
|
|
113
114
|
@click="enter(module.id, $event)"
|
|
114
115
|
@pointerenter="show(module.id)"
|
|
@@ -142,6 +143,7 @@ const badgeProps = (badge: (typeof moduleNavigationGroups.value)[number]['badge'
|
|
|
142
143
|
:icon="module.icon"
|
|
143
144
|
:color="module.id === activeModuleId ? 'primary' : 'neutral'"
|
|
144
145
|
variant="ghost"
|
|
146
|
+
:ui="mainMenuButtonUi"
|
|
145
147
|
class="min-h-11 w-full justify-start rounded-md px-3"
|
|
146
148
|
:aria-expanded="expanded.includes(module.id)"
|
|
147
149
|
:aria-controls="`sidebar-module-${module.id}`"
|
|
@@ -173,6 +175,7 @@ const badgeProps = (badge: (typeof moduleNavigationGroups.value)[number]['badge'
|
|
|
173
175
|
:aria-label="module.label"
|
|
174
176
|
:color="module.id === activeModuleId ? 'primary' : 'neutral'"
|
|
175
177
|
variant="ghost"
|
|
178
|
+
:ui="mainMenuButtonUi"
|
|
176
179
|
class="min-h-11 w-full rounded-md px-3"
|
|
177
180
|
:class="collapsed ? 'justify-center' : 'justify-start'"
|
|
178
181
|
@click="emit('navigate')"
|
|
@@ -197,6 +200,7 @@ const badgeProps = (badge: (typeof moduleNavigationGroups.value)[number]['badge'
|
|
|
197
200
|
:aria-label="item.label"
|
|
198
201
|
:color="route.path === item.to ? 'primary' : 'neutral'"
|
|
199
202
|
variant="ghost"
|
|
203
|
+
:ui="mainMenuButtonUi"
|
|
200
204
|
class="min-h-11 w-full rounded-md px-3"
|
|
201
205
|
:class="collapsed ? 'justify-center' : 'justify-start'"
|
|
202
206
|
@click="emit('navigate')"
|
package/nuxt.config.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
1
3
|
export default defineNuxtConfig({
|
|
2
4
|
$meta: { name: 'nuxt-customer-portal-ui' },
|
|
3
5
|
compatibilityDate: '2025-10-24',
|
|
4
6
|
modules: ['@nuxt/ui', '@nuxtjs/i18n', '@pinia/nuxt'],
|
|
7
|
+
css: [fileURLToPath(new URL('./app/assets/css/surfaces.css', import.meta.url))],
|
|
5
8
|
i18n: {
|
|
6
9
|
locales: [
|
|
7
10
|
{ code: 'en', file: 'en.json' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxt-customer-portal/ui",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://nuxt-customer-portal.com",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"remark-parse": "^11.0.0",
|
|
40
40
|
"unified": "^11.0.5",
|
|
41
41
|
"zod": "^4.4.3",
|
|
42
|
-
"@nuxt-customer-portal/core": "^0.10.
|
|
42
|
+
"@nuxt-customer-portal/core": "^0.10.3"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"nuxt": "^4.5.1",
|