@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 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 dropdownContent = 'bg-default ring-1 ring-accented shadow-xl shadow-black/15 dark:shadow-black/40'
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-accented data-[state=checked]:before:bg-elevated data-[state=checked]:font-medium'
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
- slots: { content: dropdownContent, item: selectItem }
79
+ ...field,
80
+ slots: { ...field.slots, content: dropdownContent, item: selectItem, arrow: 'fill-(--portal-surface)' }
19
81
  },
20
82
  selectMenu: {
21
- slots: { content: dropdownContent, item: selectItem }
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 class="w-full rounded-md border overflow-hidden" :class="color === 'error' ? 'border-error' : 'border-default'">
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.1",
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.1"
42
+ "@nuxt-customer-portal/core": "^0.10.3"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "nuxt": "^4.5.1",
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  id: 'ui',
3
- version: '0.10.1',
3
+ version: '0.10.3',
4
4
  source: '@nuxt-customer-portal/ui',
5
5
  dependsOn: ['core']
6
6
  }