@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 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.6.2",
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",
@@ -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
- const firstWrite = React.useRef(true);
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
- if (firstWrite.current) {
230
- firstWrite.current = false; // don't overwrite stored value with `initial`
231
- return;
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 {
@@ -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, companyUrl };
16
- # BrandProvider fetches it on load and overrides the values above — including the
17
- # tab title — live, no rebuild. (Or seed <BrandProvider initial={…}> from a
18
- # loader, or call useBrand().setBrand(…) once your API responds.)
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}