@navette-js/panels 1.0.0-beta.2 → 1.0.0-beta.4
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/THIRD-PARTY-NOTICES.md +1 -1
- package/lib/import-export.d.ts +142 -0
- package/lib/import-export.js +370 -0
- package/lib/index.d.ts +188 -11
- package/lib/index.js +691 -92
- package/lib/notifications.d.ts +20 -2
- package/lib/notifications.js +34 -4
- package/lib/session-store.d.ts +21 -0
- package/lib/session-store.js +35 -0
- package/lib/xlsx.d.ts +2 -0
- package/lib/xlsx.js +69 -0
- package/package.json +9 -9
- package/views/components/user-menu.ejs +3 -2
- package/views/layout/base.ejs +7 -1
- package/views/livewire/sidebar.ejs +6 -5
package/lib/notifications.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ export type NotificationActionData = {
|
|
|
3
3
|
label: string;
|
|
4
4
|
url: string | null;
|
|
5
5
|
openUrlInNewTab: boolean;
|
|
6
|
+
close?: boolean;
|
|
7
|
+
dispatch?: {
|
|
8
|
+
event: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
} | null;
|
|
11
|
+
color?: string | null;
|
|
12
|
+
button?: boolean;
|
|
6
13
|
};
|
|
7
14
|
export type NotificationData = {
|
|
8
15
|
id: string;
|
|
@@ -40,11 +47,22 @@ export declare class Notification {
|
|
|
40
47
|
iconColor(color: string): this;
|
|
41
48
|
duration(ms: number): this;
|
|
42
49
|
persistent(): this;
|
|
43
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Actions under the text: links (`url`), or buttons that `close` the notification and/or `dispatch` a Livewire event
|
|
52
|
+
* (`{ event: 'undoDelete', data: { id: 5 } }`). `button: true` renders a button instead of a link. Buttons are only shown in
|
|
53
|
+
* flash notifications; the database notifications panel shows the links.
|
|
54
|
+
*/
|
|
44
55
|
actions(actions: {
|
|
45
56
|
label: string;
|
|
46
57
|
url?: string | null;
|
|
47
58
|
openUrlInNewTab?: boolean;
|
|
59
|
+
close?: boolean;
|
|
60
|
+
dispatch?: {
|
|
61
|
+
event: string;
|
|
62
|
+
data?: Record<string, unknown>;
|
|
63
|
+
} | null;
|
|
64
|
+
color?: string | null;
|
|
65
|
+
button?: boolean;
|
|
48
66
|
}[]): this;
|
|
49
67
|
getData(): NotificationData;
|
|
50
68
|
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
@@ -52,7 +70,7 @@ export declare class Notification {
|
|
|
52
70
|
/** Queues the notification for the browser of the current request. */
|
|
53
71
|
send(): this;
|
|
54
72
|
}
|
|
55
|
-
/** Renders a flash notification (no date
|
|
73
|
+
/** Renders a flash notification (no date or inline mode). Title, body and action labels are escaped. */
|
|
56
74
|
export declare function renderNotification(n: NotificationData): string;
|
|
57
75
|
/** Container component for flash notifications. */
|
|
58
76
|
export declare class Notifications extends Component {
|
package/lib/notifications.js
CHANGED
|
@@ -53,8 +53,24 @@ export class Notification {
|
|
|
53
53
|
iconColor(color) { this.data.iconColor = color; return this; }
|
|
54
54
|
duration(ms) { this.data.duration = ms; return this; }
|
|
55
55
|
persistent() { this.data.duration = 'persistent'; return this; }
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Actions under the text: links (`url`), or buttons that `close` the notification and/or `dispatch` a Livewire event
|
|
58
|
+
* (`{ event: 'undoDelete', data: { id: 5 } }`). `button: true` renders a button instead of a link. Buttons are only shown in
|
|
59
|
+
* flash notifications; the database notifications panel shows the links.
|
|
60
|
+
*/
|
|
61
|
+
actions(actions) {
|
|
62
|
+
this.data.actions = actions.slice(0, 10).map((action) => {
|
|
63
|
+
const event = action.dispatch?.event;
|
|
64
|
+
if (event !== undefined && !/^[A-Za-z0-9_.:-]{1,100}$/.test(event))
|
|
65
|
+
throw new Error(`Invalid notification event name: ${event}`);
|
|
66
|
+
return {
|
|
67
|
+
label: String(action.label), url: action.url ?? null, openUrlInNewTab: action.openUrlInNewTab === true, close: action.close === true,
|
|
68
|
+
dispatch: event ? { event, data: action.dispatch?.data ?? {} } : null,
|
|
69
|
+
color: action.color && /^[a-z]+$/.test(action.color) ? action.color : null, button: action.button === true,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
58
74
|
getData() { return { ...this.data }; }
|
|
59
75
|
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
60
76
|
async sendToDatabase(store, user) { if (store)
|
|
@@ -68,7 +84,21 @@ export class Notification {
|
|
|
68
84
|
return this;
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
|
-
/**
|
|
87
|
+
/** Actions of a notification: links (only local or http(s) URLs) and buttons closing it or dispatching a Livewire event. */
|
|
88
|
+
function notificationActionsHtml(n) {
|
|
89
|
+
const safeUrl = (url) => typeof url === 'string' && /^(\/(?!\/)|https?:\/\/)/.test(url);
|
|
90
|
+
const items = (n.actions ?? []).filter((action) => action.label !== '' && (safeUrl(action.url) || action.close || action.dispatch)).map((action) => {
|
|
91
|
+
const color = action.color ?? 'primary';
|
|
92
|
+
const classes = action.button ? `fi-ac-btn-action fi-btn fi-size-sm` : `fi-ac-link-action fi-link fi-size-sm fi-color fi-color-${color} fi-text-color-600 dark:fi-text-color-400`;
|
|
93
|
+
const label = action.button ? e(action.label) : `<span class="fi-link-label">${e(action.label)}</span>`;
|
|
94
|
+
const clicks = [action.dispatch ? `Livewire.dispatch(${js(action.dispatch.event)}, ${js(action.dispatch.data)})` : null, action.close ? 'close()' : null].filter(Boolean).join('; ');
|
|
95
|
+
if (safeUrl(action.url))
|
|
96
|
+
return `<a href="${e(action.url)}"${action.openUrlInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ''}${clicks ? ` x-on:click="${e(clicks)}"` : ''} class="${classes}">${label}</a>`;
|
|
97
|
+
return `<button type="button" x-on:click="${e(clicks)}" class="${classes}">${label}</button>`;
|
|
98
|
+
});
|
|
99
|
+
return items.length ? `<div class="fi-ac fi-no-notification-actions">${items.join('')}</div>` : '';
|
|
100
|
+
}
|
|
101
|
+
/** Renders a flash notification (no date or inline mode). Title, body and action labels are escaped. */
|
|
72
102
|
export function renderNotification(n) {
|
|
73
103
|
const closeLabel = t('filament-notifications::notification.actions.close.label');
|
|
74
104
|
const attributes = new Attributes()
|
|
@@ -77,7 +107,7 @@ export function renderNotification(n) {
|
|
|
77
107
|
const iconName = n.icon ?? (n.status ? STATUS_ICON[n.status] : null);
|
|
78
108
|
const iconColor = n.iconColor ?? n.status;
|
|
79
109
|
const iconHtml = iconName ? icon(iconName, { size: 'lg', attributes: new Attributes().class([...(iconColor && iconColor !== 'gray' ? ['fi-color', `fi-color-${iconColor}`] : []), 'fi-no-notification-icon']) }) : '';
|
|
80
|
-
return `<div x-data="notificationComponent({ notification: ${e(js({ id: n.id, duration: n.duration }))} })" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" x-on:mouseenter="pauseDuration('hover')" x-on:mouseleave="resumeDuration('hover')" x-on:focusin="handleFocusIn($event)" x-on:focusout="handleFocusOut($event)" x-on:keydown.escape="handleEscape($event)" ${attributes.toHtml().replace(/"if \(\$event\.detail\.id == '([^']*)'\) close\(\)"/, (_m, id) => `"if ($event.detail.id == '${id}') close()"`)}>${iconHtml}<div class="fi-no-notification-main">${n.title !== null || n.body !== null ? `<div class="fi-no-notification-text">${n.title !== null ? `<h3 class="fi-no-notification-title">${e(n.title)}</h3>` : ''}${n.body !== null ? `<div class="fi-no-notification-body">${e(n.body)}</div>` : ''}</div>` : ''}</div><button type="button" x-on:click="dismiss()" aria-label="${e(closeLabel)}" title="${e(closeLabel)}" class="fi-icon-btn fi-no-notification-close-btn">${icon('m-x-mark')}</button></div>`;
|
|
110
|
+
return `<div x-data="notificationComponent({ notification: ${e(js({ id: n.id, duration: n.duration }))} })" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" x-on:mouseenter="pauseDuration('hover')" x-on:mouseleave="resumeDuration('hover')" x-on:focusin="handleFocusIn($event)" x-on:focusout="handleFocusOut($event)" x-on:keydown.escape="handleEscape($event)" ${attributes.toHtml().replace(/"if \(\$event\.detail\.id == '([^']*)'\) close\(\)"/, (_m, id) => `"if ($event.detail.id == '${id}') close()"`)}>${iconHtml}<div class="fi-no-notification-main">${n.title !== null || n.body !== null ? `<div class="fi-no-notification-text">${n.title !== null ? `<h3 class="fi-no-notification-title">${e(n.title)}</h3>` : ''}${n.body !== null ? `<div class="fi-no-notification-body">${e(n.body)}</div>` : ''}</div>` : ''}${notificationActionsHtml(n)}</div><button type="button" x-on:click="dismiss()" aria-label="${e(closeLabel)}" title="${e(closeLabel)}" class="fi-icon-btn fi-no-notification-close-btn">${icon('m-x-mark')}</button></div>`;
|
|
81
111
|
}
|
|
82
112
|
/** Container component for flash notifications. */
|
|
83
113
|
export class Notifications extends Component {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Storage of small per-session values. Implement it to keep them in a database or a cache shared by several processes. */
|
|
2
|
+
export interface SessionStore {
|
|
3
|
+
get(session: string, key: string): unknown | Promise<unknown>;
|
|
4
|
+
set(session: string, key: string, value: unknown): void | Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* In-memory store: values are lost when the process restarts and are not shared between processes.
|
|
8
|
+
* Sessions idle for longer than `ttl` are dropped, and the oldest sessions go first beyond `maxSessions`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MemorySessionStore implements SessionStore {
|
|
11
|
+
private sessions;
|
|
12
|
+
private readonly options;
|
|
13
|
+
constructor(options?: {
|
|
14
|
+
ttl?: number;
|
|
15
|
+
maxSessions?: number;
|
|
16
|
+
maxValueLength?: number;
|
|
17
|
+
});
|
|
18
|
+
private get ttl();
|
|
19
|
+
get(session: string, key: string): unknown;
|
|
20
|
+
set(session: string, key: string, value: unknown): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Per-browser session data (persisted table filters, search and sort). The default store keeps it in memory.
|
|
2
|
+
/**
|
|
3
|
+
* In-memory store: values are lost when the process restarts and are not shared between processes.
|
|
4
|
+
* Sessions idle for longer than `ttl` are dropped, and the oldest sessions go first beyond `maxSessions`.
|
|
5
|
+
*/
|
|
6
|
+
export class MemorySessionStore {
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
options;
|
|
9
|
+
constructor(options = {}) { this.options = options; }
|
|
10
|
+
get ttl() { return this.options.ttl ?? 12 * 60 * 60 * 1000; }
|
|
11
|
+
get(session, key) {
|
|
12
|
+
const entry = this.sessions.get(session);
|
|
13
|
+
if (!entry || Date.now() - entry.at > this.ttl) {
|
|
14
|
+
this.sessions.delete(session);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const raw = entry.values.get(key);
|
|
18
|
+
return raw === undefined ? undefined : JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
set(session, key, value) {
|
|
21
|
+
const raw = JSON.stringify(value ?? null);
|
|
22
|
+
// Values come from the browser: an oversized one is not kept.
|
|
23
|
+
if (raw.length > (this.options.maxValueLength ?? 64 * 1024))
|
|
24
|
+
return;
|
|
25
|
+
const entry = this.sessions.get(session) ?? { at: Date.now(), values: new Map() };
|
|
26
|
+
entry.at = Date.now();
|
|
27
|
+
entry.values.set(key, raw);
|
|
28
|
+
// Most recently used last: re-inserting moves the session to the end.
|
|
29
|
+
this.sessions.delete(session);
|
|
30
|
+
this.sessions.set(session, entry);
|
|
31
|
+
const max = this.options.maxSessions ?? 10000;
|
|
32
|
+
while (this.sessions.size > max)
|
|
33
|
+
this.sessions.delete(this.sessions.keys().next().value);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/lib/xlsx.d.ts
ADDED
package/lib/xlsx.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Minimal XLSX writer: one worksheet of text cells (inline strings), zipped with Node's zlib. Cells are never formulas, so
|
|
2
|
+
// spreadsheet software does not evaluate them; numbers stay text, as in Filament's export.
|
|
3
|
+
import { crc32, deflateRawSync } from 'node:zlib';
|
|
4
|
+
/** Escapes a value for XML; characters not allowed in XML 1.0 are dropped. */
|
|
5
|
+
const xml = (value) => value.replace(/[^\x09\x0A\x0D\x20--�\u{10000}-\u{10FFFF}]/gu, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
6
|
+
/** Column letters of a 0-based index: 0 → A, 26 → AA. */
|
|
7
|
+
function columnName(index) {
|
|
8
|
+
let name = '';
|
|
9
|
+
for (let n = index + 1; n > 0; n = Math.floor((n - 1) / 26))
|
|
10
|
+
name = String.fromCharCode(65 + ((n - 1) % 26)) + name;
|
|
11
|
+
return name;
|
|
12
|
+
}
|
|
13
|
+
/** Workbook with a single sheet: the first row is bold (header). */
|
|
14
|
+
export function buildXlsx(rows, sheetName = 'Sheet1') {
|
|
15
|
+
const sheetRows = rows.map((row, r) => `<row r="${r + 1}">${row.map((cell, c) => `<c r="${columnName(c)}${r + 1}" t="inlineStr"${r === 0 ? ' s="1"' : ''}><is><t xml:space="preserve">${xml(cell === null || cell === undefined ? '' : String(cell)).slice(0, 32767)}</t></is></c>`).join('')}</row>`).join('');
|
|
16
|
+
const files = [
|
|
17
|
+
['[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/></Types>'],
|
|
18
|
+
['_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'],
|
|
19
|
+
['xl/workbook.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="${xml(sheetName.replace(/[\\/?*[\]:]/g, ' ').slice(0, 31)) || 'Sheet1'}" sheetId="1" r:id="rId1"/></sheets></workbook>`],
|
|
20
|
+
['xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>'],
|
|
21
|
+
['xl/styles.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs></styleSheet>'],
|
|
22
|
+
['xl/worksheets/sheet1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${sheetRows}</sheetData></worksheet>`],
|
|
23
|
+
];
|
|
24
|
+
return zip(files.map(([name, content]) => [name, Buffer.from(content, 'utf8')]));
|
|
25
|
+
}
|
|
26
|
+
/** ZIP archive (deflate), as required by the XLSX format. */
|
|
27
|
+
function zip(entries) {
|
|
28
|
+
const locals = [], centrals = [];
|
|
29
|
+
let offset = 0;
|
|
30
|
+
for (const [name, data] of entries) {
|
|
31
|
+
const nameBuffer = Buffer.from(name, 'utf8');
|
|
32
|
+
const compressed = deflateRawSync(data);
|
|
33
|
+
const crc = crc32(data);
|
|
34
|
+
const local = Buffer.alloc(30);
|
|
35
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
36
|
+
local.writeUInt16LE(20, 4);
|
|
37
|
+
local.writeUInt16LE(0x0800, 6);
|
|
38
|
+
local.writeUInt16LE(8, 8);
|
|
39
|
+
local.writeUInt32LE(0, 10);
|
|
40
|
+
local.writeUInt32LE(crc >>> 0, 14);
|
|
41
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
42
|
+
local.writeUInt32LE(data.length, 22);
|
|
43
|
+
local.writeUInt16LE(nameBuffer.length, 26);
|
|
44
|
+
local.writeUInt16LE(0, 28);
|
|
45
|
+
const central = Buffer.alloc(46);
|
|
46
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
47
|
+
central.writeUInt16LE(20, 4);
|
|
48
|
+
central.writeUInt16LE(20, 6);
|
|
49
|
+
central.writeUInt16LE(0x0800, 8);
|
|
50
|
+
central.writeUInt16LE(8, 10);
|
|
51
|
+
central.writeUInt32LE(0, 12);
|
|
52
|
+
central.writeUInt32LE(crc >>> 0, 16);
|
|
53
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
54
|
+
central.writeUInt32LE(data.length, 24);
|
|
55
|
+
central.writeUInt16LE(nameBuffer.length, 28);
|
|
56
|
+
central.writeUInt32LE(offset, 42);
|
|
57
|
+
locals.push(local, nameBuffer, compressed);
|
|
58
|
+
centrals.push(central, nameBuffer);
|
|
59
|
+
offset += local.length + nameBuffer.length + compressed.length;
|
|
60
|
+
}
|
|
61
|
+
const centralSize = centrals.reduce((size, buffer) => size + buffer.length, 0);
|
|
62
|
+
const end = Buffer.alloc(22);
|
|
63
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
64
|
+
end.writeUInt16LE(entries.length, 8);
|
|
65
|
+
end.writeUInt16LE(entries.length, 10);
|
|
66
|
+
end.writeUInt32LE(centralSize, 12);
|
|
67
|
+
end.writeUInt32LE(offset, 16);
|
|
68
|
+
return Buffer.concat([...locals, ...centrals, end]);
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/panels",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"description": "Admin panels for Node.js: resources, pages, navigation, authentication, tenancy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"node"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@navette-js/support": "1.0.0-beta.
|
|
47
|
-
"@navette-js/schemas": "1.0.0-beta.
|
|
48
|
-
"@navette-js/forms": "1.0.0-beta.
|
|
49
|
-
"@navette-js/actions": "1.0.0-beta.
|
|
50
|
-
"@navette-js/wire": "1.0.0-beta.
|
|
51
|
-
"@navette-js/tables": "1.0.0-beta.
|
|
52
|
-
"@navette-js/infolists": "1.0.0-beta.
|
|
53
|
-
"@navette-js/database": "1.0.0-beta.
|
|
46
|
+
"@navette-js/support": "1.0.0-beta.4",
|
|
47
|
+
"@navette-js/schemas": "1.0.0-beta.4",
|
|
48
|
+
"@navette-js/forms": "1.0.0-beta.4",
|
|
49
|
+
"@navette-js/actions": "1.0.0-beta.4",
|
|
50
|
+
"@navette-js/wire": "1.0.0-beta.4",
|
|
51
|
+
"@navette-js/tables": "1.0.0-beta.4",
|
|
52
|
+
"@navette-js/infolists": "1.0.0-beta.4",
|
|
53
|
+
"@navette-js/database": "1.0.0-beta.4",
|
|
54
54
|
"express": "^5.1.0"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
<% } %>
|
|
32
32
|
<% if (before.length) { %> <div class="fi-dropdown-list"><%- before.join('') %></div>
|
|
33
33
|
<% } %>
|
|
34
|
-
<div class="fi-dropdown-list">
|
|
34
|
+
<% if (locals.hasThemeSwitcher !== false) { %> <div class="fi-dropdown-list">
|
|
35
35
|
<div x-data="{ theme: null }" x-init="
|
|
36
36
|
$watch('theme', () => {
|
|
37
37
|
$dispatch('theme-changed', theme)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
theme = localStorage.getItem('filament:theme:mode') || 'system' " role="group" aria-label="<%= t('filament-panels::layout.actions.theme_switcher.label') %>" class="fi-theme-switcher">
|
|
40
|
+
theme = localStorage.getItem('filament:theme:mode') || <%- js(locals.defaultThemeMode ?? 'system') %> " role="group" aria-label="<%= t('filament-panels::layout.actions.theme_switcher.label') %>" class="fi-theme-switcher">
|
|
41
41
|
<% for (const [theme, iconName] of [['light', 'm-sun'], ['dark', 'm-moon'], ['system', 'm-computer-desktop']]) { %>
|
|
42
42
|
<% const label = t('filament-panels::layout.actions.theme_switcher.' + theme + '.label') %>
|
|
43
43
|
<button aria-label="<%= label %>" role="menuitemradio" tabindex="-1" type="button" x-on:click="(theme = <%- js(theme) %>) && close()" x-tooltip="{
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
<% } %>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
52
|
+
<% } %>
|
|
52
53
|
|
|
53
54
|
<% if (locals.themes && themes.length > 1) { %>
|
|
54
55
|
<%# Choice of one of the panel's predefined themes (Panel.themes()). %>
|
package/views/layout/base.ejs
CHANGED
|
@@ -68,10 +68,15 @@
|
|
|
68
68
|
</style>
|
|
69
69
|
|
|
70
70
|
<%- renderHook('panels::styles.after', renderHookScopes) %>
|
|
71
|
+
<% if (locals.darkMode === false) { %><% } else if (locals.darkModeForced) { %>
|
|
72
|
+
<script>
|
|
73
|
+
document.documentElement.classList.add('dark')
|
|
74
|
+
</script>
|
|
75
|
+
<% } else { %>
|
|
71
76
|
<script>
|
|
72
77
|
const loadDarkMode = () => {
|
|
73
78
|
window.theme =
|
|
74
|
-
localStorage.getItem('filament:theme:mode') ?? 'system'
|
|
79
|
+
localStorage.getItem('filament:theme:mode') ?? <%- js(locals.defaultThemeMode ?? 'system') %>
|
|
75
80
|
|
|
76
81
|
if (
|
|
77
82
|
window.theme === 'dark' ||
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
|
|
88
93
|
document.addEventListener('livewire:navigated', loadDarkMode)
|
|
89
94
|
</script>
|
|
95
|
+
<% } %>
|
|
90
96
|
|
|
91
97
|
<% if (locals.themeCss) { %><style data-navette-theme><%- themeCss %></style><% } %>
|
|
92
98
|
|
|
@@ -42,19 +42,20 @@
|
|
|
42
42
|
<%- renderHook('panels::sidebar.nav.start') %>
|
|
43
43
|
<ul class="fi-sidebar-nav-groups">
|
|
44
44
|
<% for (const group of groups) { %>
|
|
45
|
-
<li x-data="{ label: <%- js(group.label ?? '') %> }" data-group-label="<%= group.label ?? '' %>" x-bind:class="{ 'fi-collapsed': $store.sidebar.groupIsCollapsed(label) }" class="<%- cls(['fi-sidebar-group', { 'fi-active': group.isActive
|
|
45
|
+
<li x-data="{ label: <%- js(group.label ?? '') %> }" data-group-label="<%= group.label ?? '' %>" x-bind:class="{ 'fi-collapsed': $store.sidebar.groupIsCollapsed(label) }" class="<%- cls(['fi-sidebar-group', { 'fi-active': group.isActive, 'fi-collapsible': group.collapsible !== false }]) %>">
|
|
46
46
|
<% if (group.label) { %>
|
|
47
|
-
<div x-on:click="$store.sidebar.toggleCollapsedGroup(label)" class="fi-sidebar-group-btn">
|
|
47
|
+
<div<% if (group.collapsible !== false) { %> x-on:click="$store.sidebar.toggleCollapsedGroup(label)"<% } %> class="fi-sidebar-group-btn">
|
|
48
|
+
<% if (group.icon) { %><%- icon(group.icon, { size: 'lg' }) %><% } %>
|
|
48
49
|
<span class="fi-sidebar-group-label">
|
|
49
50
|
<%= group.label %>
|
|
50
51
|
</span>
|
|
51
|
-
|
|
52
|
+
<% if (group.collapsible !== false) { %><%- view('filament::components.icon-button', {
|
|
52
53
|
iconName: 'm-chevron-up', label: group.label, className: 'fi-sidebar-group-collapse-btn',
|
|
53
54
|
extra: attrs({ 'aria-controls': group.itemsId, 'x-bind:aria-expanded': '! $store.sidebar.groupIsCollapsed(label)', 'x-on:click.stop': '$store.sidebar.toggleCollapsedGroup(label)' }),
|
|
54
|
-
}) %>
|
|
55
|
+
}) %><% } %>
|
|
55
56
|
</div>
|
|
56
57
|
<% } %>
|
|
57
|
-
<ul<% if (group.label) { %> id="<%= group.itemsId %>" x-show="! $store.sidebar.groupIsCollapsed(label)" x-collapse.duration.200ms<% } %> class="fi-sidebar-group-items">
|
|
58
|
+
<ul<% if (group.label) { %> id="<%= group.itemsId %>"<% if (group.collapsible !== false) { %> x-show="! $store.sidebar.groupIsCollapsed(label)" x-collapse.duration.200ms<% } %><% } %> class="fi-sidebar-group-items">
|
|
58
59
|
<% for (const item of group.items) { %>
|
|
59
60
|
<li class="<%- cls(['fi-sidebar-item', { 'fi-active': item.isActive }, 'fi-sidebar-item-has-url']) %>">
|
|
60
61
|
<a href="<%= item.url %>"<% if (spa) { %> wire:navigate<% } %><% if (item.isActive) { %> aria-current="page"<% } %> x-on:click="$store.sidebar.isMobile && $store.sidebar.close()" class="fi-sidebar-item-btn">
|