@viliha/vui-ui 1.6.2 → 1.7.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 +22 -0
- package/package.json +1 -1
- package/src/record-view.tsx +10 -5
- package/template/.env.example +9 -4
- package/template/app/_components/brand.tsx +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ backward-compatible features, **major** for breaking changes.
|
|
|
7
7
|
|
|
8
8
|
To upgrade, see [Upgrading](./AGENT.md#upgrading) in the agent guide.
|
|
9
9
|
|
|
10
|
+
## 1.7.0 — 2026-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Configurable favicon** via the runtime brand system. `BrandProvider` gains a
|
|
15
|
+
`faviconUrl` field (alongside `logoUrl`): set `NEXT_PUBLIC_FAVICON_URL` for a
|
|
16
|
+
build-time default, return `faviconUrl` from your `NEXT_PUBLIC_BRAND_URL` JSON
|
|
17
|
+
(or call `useBrand().setBrand({ faviconUrl })`) to change the browser-tab icon
|
|
18
|
+
live per tenant, no rebuild. The static `app/icon.*` files remain the fallback.
|
|
19
|
+
|
|
20
|
+
## 1.6.3 — 2026-07-25
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `RecordForm` no longer loses in-progress input when switching tabs **in dev**.
|
|
25
|
+
The draft persistence (`persistKey`) used a first-write flag that React
|
|
26
|
+
StrictMode's effect double-invoke defeated: the second pass wrote the empty
|
|
27
|
+
initial value over the draft the restore effect was about to bring back. The
|
|
28
|
+
writer now skips the untouched seed by identity, so it never clobbers a stored
|
|
29
|
+
draft. (Production builds, which don't run StrictMode's double-invoke, were
|
|
30
|
+
unaffected.)
|
|
31
|
+
|
|
10
32
|
## 1.6.2 — 2026-07-25
|
|
11
33
|
|
|
12
34
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viliha/vui-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Vui UI — a clean, token-driven React admin/CRM component library built on Tailwind CSS v4, shadcn-style patterns, and Radix Icons. Ships as TypeScript source (Just-in-Time), compiled by the consuming app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Suman Bonakurthi",
|
package/src/record-view.tsx
CHANGED
|
@@ -214,7 +214,13 @@ function usePersistentState<T>(
|
|
|
214
214
|
initial: T,
|
|
215
215
|
): [T, React.Dispatch<React.SetStateAction<T>>] {
|
|
216
216
|
const [state, setState] = React.useState<T>(initial);
|
|
217
|
-
|
|
217
|
+
// Stable reference to the seeded value. The writer skips it by identity (not a
|
|
218
|
+
// first-write flag) so the write is StrictMode-safe: dev double-invokes the
|
|
219
|
+
// write effect while `state` is still `initial`, and a flag flips on the first
|
|
220
|
+
// pass so the second pass would persist `initial` OVER a draft the restore
|
|
221
|
+
// effect is about to bring back. Comparing to this ref never writes `initial`,
|
|
222
|
+
// so it can't clobber the stored draft.
|
|
223
|
+
const initialRef = React.useRef(initial);
|
|
218
224
|
React.useEffect(() => {
|
|
219
225
|
if (!key) return;
|
|
220
226
|
try {
|
|
@@ -226,10 +232,9 @@ function usePersistentState<T>(
|
|
|
226
232
|
}, [key]);
|
|
227
233
|
React.useEffect(() => {
|
|
228
234
|
if (!key) return;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
235
|
+
// Nothing to save while still the untouched seed; only a restore or a user
|
|
236
|
+
// edit diverges `state` from it, and both should persist.
|
|
237
|
+
if (state === initialRef.current) return;
|
|
233
238
|
try {
|
|
234
239
|
sessionStorage.setItem(key, JSON.stringify(state));
|
|
235
240
|
} catch {
|
package/template/.env.example
CHANGED
|
@@ -12,10 +12,11 @@ NEXT_PUBLIC_APP_DESCRIPTION="Your app description for search engines and social
|
|
|
12
12
|
NEXT_PUBLIC_APP_URL="https://vui.viliha.com"
|
|
13
13
|
|
|
14
14
|
# Runtime branding (multi-tenant / white-label). Point this at a JSON endpoint
|
|
15
|
-
# returning any of { name, tagline, description, logoUrl, company,
|
|
16
|
-
# BrandProvider fetches it on load and overrides the values above —
|
|
17
|
-
# tab title — live, no rebuild. (Or seed
|
|
18
|
-
# loader, or call useBrand().setBrand(…) once
|
|
15
|
+
# returning any of { name, tagline, description, logoUrl, faviconUrl, company,
|
|
16
|
+
# companyUrl }; BrandProvider fetches it on load and overrides the values above —
|
|
17
|
+
# including the tab title and favicon — live, no rebuild. (Or seed
|
|
18
|
+
# <BrandProvider initial={…}> from a loader, or call useBrand().setBrand(…) once
|
|
19
|
+
# your API responds.)
|
|
19
20
|
# NEXT_PUBLIC_BRAND_URL="https://api.example.com/branding"
|
|
20
21
|
|
|
21
22
|
NEXT_PUBLIC_COMPANY_NAME="VILIHA PTE. LTD."
|
|
@@ -28,6 +29,10 @@ NEXT_PUBLIC_LICENSE="MIT Licensed"
|
|
|
28
29
|
# mark. e.g. drop public/logo.svg and set:
|
|
29
30
|
# NEXT_PUBLIC_LOGO_URL="/logo.svg"
|
|
30
31
|
|
|
32
|
+
# Optional — browser-tab icon (favicon). Overrides the static app/icon.* files;
|
|
33
|
+
# falls back to them when unset. Serve from /public or use an absolute URL.
|
|
34
|
+
# NEXT_PUBLIC_FAVICON_URL="/favicon.png"
|
|
35
|
+
|
|
31
36
|
# Override the entire footer line at once (takes precedence over the vars above):
|
|
32
37
|
# NEXT_PUBLIC_FOOTER_NOTICE="© 2026 Acme Inc. · All rights reserved"
|
|
33
38
|
|
|
@@ -26,6 +26,9 @@ export type Brand = {
|
|
|
26
26
|
description: string;
|
|
27
27
|
/** Overrides NEXT_PUBLIC_LOGO_URL at runtime; unset → the built-in mark. */
|
|
28
28
|
logoUrl?: string;
|
|
29
|
+
/** Browser-tab icon. Overrides NEXT_PUBLIC_FAVICON_URL at runtime; unset →
|
|
30
|
+
* the static app/icon.* file convention. */
|
|
31
|
+
faviconUrl?: string;
|
|
29
32
|
company: string;
|
|
30
33
|
companyUrl: string;
|
|
31
34
|
};
|
|
@@ -35,6 +38,7 @@ const DEFAULT_BRAND: Brand = {
|
|
|
35
38
|
tagline: SITE.tagline,
|
|
36
39
|
description: SITE.description,
|
|
37
40
|
logoUrl: process.env.NEXT_PUBLIC_LOGO_URL || undefined,
|
|
41
|
+
faviconUrl: process.env.NEXT_PUBLIC_FAVICON_URL || undefined,
|
|
38
42
|
company: SITE.company,
|
|
39
43
|
companyUrl: SITE.companyUrl,
|
|
40
44
|
};
|
|
@@ -106,6 +110,32 @@ export function BrandProvider({
|
|
|
106
110
|
return () => cancelAnimationFrame(id);
|
|
107
111
|
}, [pathname, brand.name, brand.tagline]);
|
|
108
112
|
|
|
113
|
+
// Swap the browser-tab icon when the brand provides one (env default or API).
|
|
114
|
+
// The static app/icon.* links are the pre-JS fallback; here we point every
|
|
115
|
+
// icon <link> at the brand favicon and restore them if it changes/unmounts.
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
const href = brand.faviconUrl;
|
|
118
|
+
if (!href) return; // no override — keep the static file-convention icons
|
|
119
|
+
const links = Array.from(
|
|
120
|
+
document.querySelectorAll<HTMLLinkElement>("link[rel~='icon']"),
|
|
121
|
+
);
|
|
122
|
+
if (links.length === 0) {
|
|
123
|
+
const link = document.createElement("link");
|
|
124
|
+
link.rel = "icon";
|
|
125
|
+
document.head.appendChild(link);
|
|
126
|
+
links.push(link);
|
|
127
|
+
}
|
|
128
|
+
const prev = links.map((l) => l.getAttribute("href"));
|
|
129
|
+
links.forEach((l) => l.setAttribute("href", href));
|
|
130
|
+
return () => {
|
|
131
|
+
links.forEach((l, i) => {
|
|
132
|
+
const p = prev[i];
|
|
133
|
+
if (p == null) l.remove(); // was created/absent → drop it
|
|
134
|
+
else l.setAttribute("href", p);
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}, [brand.faviconUrl]);
|
|
138
|
+
|
|
109
139
|
return (
|
|
110
140
|
<BrandContext.Provider value={{ brand, setBrand }}>
|
|
111
141
|
{children}
|