@navette-js/support 1.0.0-beta.1 → 1.0.0-beta.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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/lib/i18n.d.ts +3 -3
- package/lib/i18n.js +5 -5
- package/lib/index.d.ts +41 -40
- package/lib/index.js +60 -62
- package/lib/ui.d.ts +27 -27
- package/lib/ui.js +33 -36
- package/package.json +1 -2
- package/views/components/badge.ejs +1 -1
- package/views/components/icon-button.ejs +2 -2
- package/views/components/pagination.ejs +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shared utilities, colors, icons, views and translations for Navette.
|
|
4
4
|
|
|
5
|
-
Part of [Navette](https://github.com/NavetteJS/navette),
|
|
5
|
+
Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
|
|
6
6
|
|
|
7
7
|
**Beta:** the API may still change between beta releases.
|
|
8
8
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
|
-
Navette is an independent
|
|
3
|
+
Navette is an independent project, not affiliated with or endorsed by the Filament, Livewire or Heroicons projects. It reuses the following works, each under its own license; the full license texts ship next to the files they cover.
|
|
4
4
|
|
|
5
5
|
| Work | Used for | License | License file |
|
|
6
6
|
|---|---|---|---|
|
|
7
|
-
| [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup
|
|
7
|
+
| [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup derived from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
|
|
8
8
|
| [Livewire](https://github.com/livewire/livewire) © Caleb Porzio | Client script `livewire.js` (`@navette-js/wire/vendor`); the server runtime follows its protocol | MIT | `packages/wire/vendor/LIVEWIRE-LICENSE.md` |
|
|
9
9
|
| [Heroicons](https://github.com/tailwindlabs/heroicons) | SVG icons (`@navette-js/support/icons`) | MIT | `packages/support/icons/HEROICONS-LICENSE.md` |
|
|
10
10
|
| [Inter](https://github.com/rsms/inter) © The Inter Project Authors | Default font files (`@navette-js/panels/dist/filament/fonts`) | SIL Open Font License 1.1 | `packages/panels/dist/filament/fonts/INTER-LICENSE.txt` |
|
package/lib/i18n.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export declare const setLocale: (locale: string) => void;
|
|
2
2
|
export declare const getLocale: () => string;
|
|
3
|
-
/** `t('filament-panels::auth/pages/login.form.email.label')
|
|
3
|
+
/** Translates a key such as `t('filament-panels::auth/pages/login.form.email.label')`; returns the key when it is missing. */
|
|
4
4
|
export declare function t(key: string, params?: Record<string, string | number>, locale?: string): string;
|
|
5
|
-
/**
|
|
5
|
+
/** Relative past duration in English, e.g. "2 months ago". Dates without a time zone are read as UTC. */
|
|
6
6
|
export declare function diffForHumans(value: string | Date | null | undefined, now?: Date): string | null;
|
|
7
|
-
/**
|
|
7
|
+
/** Pluralized translation: `{0} …|{1} …|[2,*] …` or `singular|plural`. `:count` is always available. */
|
|
8
8
|
export declare function tc(key: string, count: number, params?: Record<string, string | number>, locale?: string): string;
|
package/lib/i18n.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Translations: `namespace::group.key` keys, `:name` placeholders, English fallback.
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
const bundle = JSON.parse(readFileSync(fileURLToPath(new URL('../data/lang.en-fr.json', import.meta.url)), 'utf8'));
|
|
@@ -9,7 +9,7 @@ const NAMESPACES = {
|
|
|
9
9
|
let currentLocale = 'en';
|
|
10
10
|
export const setLocale = (locale) => { currentLocale = locale; };
|
|
11
11
|
export const getLocale = () => currentLocale;
|
|
12
|
-
/** `t('filament-panels::auth/pages/login.form.email.label')
|
|
12
|
+
/** Translates a key such as `t('filament-panels::auth/pages/login.form.email.label')`; returns the key when it is missing. */
|
|
13
13
|
export function t(key, params = {}, locale = currentLocale) {
|
|
14
14
|
const [ns, rest] = key.split('::');
|
|
15
15
|
const dot = rest.indexOf('.');
|
|
@@ -21,7 +21,7 @@ export function t(key, params = {}, locale = currentLocale) {
|
|
|
21
21
|
}
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/** Relative past duration in English, e.g. "2 months ago". Dates without a time zone are read as UTC. */
|
|
25
25
|
export function diffForHumans(value, now = new Date()) {
|
|
26
26
|
if (value === null || value === undefined || value === '')
|
|
27
27
|
return null;
|
|
@@ -39,7 +39,7 @@ export function diffForHumans(value, now = new Date()) {
|
|
|
39
39
|
const days = Math.floor(seconds / 86400);
|
|
40
40
|
if (days < 7)
|
|
41
41
|
return unit(days, 'day');
|
|
42
|
-
//
|
|
42
|
+
// Whole calendar months elapsed, not 30-day blocks.
|
|
43
43
|
let months = (now.getUTCFullYear() - date.getUTCFullYear()) * 12 + now.getUTCMonth() - date.getUTCMonth();
|
|
44
44
|
if (now.getUTCDate() < date.getUTCDate() || (now.getUTCDate() === date.getUTCDate() && now.getTime() % 86400000 < date.getTime() % 86400000))
|
|
45
45
|
months--;
|
|
@@ -49,7 +49,7 @@ export function diffForHumans(value, now = new Date()) {
|
|
|
49
49
|
return unit(months, 'month');
|
|
50
50
|
return unit(Math.floor(months / 12), 'year');
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
52
|
+
/** Pluralized translation: `{0} …|{1} …|[2,*] …` or `singular|plural`. `:count` is always available. */
|
|
53
53
|
export function tc(key, count, params = {}, locale = currentLocale) {
|
|
54
54
|
const segments = t(key, {}, locale).split('|');
|
|
55
55
|
let chosen = null;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
/** Escapes `&`, `<`, `>`, `"` and `'` for HTML. */
|
|
1
2
|
export declare function e(value: unknown): string;
|
|
2
|
-
/**
|
|
3
|
+
/** Trusted HTML string: views do not escape it again. */
|
|
3
4
|
export declare class HtmlString {
|
|
4
5
|
html: string;
|
|
5
6
|
constructor(html: string);
|
|
6
7
|
toString(): string;
|
|
7
8
|
}
|
|
8
9
|
export declare const raw: (html: string) => HtmlString;
|
|
9
|
-
/**
|
|
10
|
+
/** Serializes a value into a JavaScript expression that is safe to embed in HTML. */
|
|
10
11
|
export declare function js(value: unknown): string;
|
|
11
12
|
export type ClassList = (string | false | null | undefined | Record<string, unknown>)[] | Record<string, unknown>;
|
|
12
13
|
export declare function cls(list: ClassList): string;
|
|
@@ -16,20 +17,20 @@ export declare class Attributes {
|
|
|
16
17
|
constructor(initial?: Record<string, AttrValue>);
|
|
17
18
|
get(key: string): AttrValue;
|
|
18
19
|
has(key: string): boolean;
|
|
19
|
-
/**
|
|
20
|
+
/** Defaults come first, then existing attributes (`class` and `style` are concatenated); null/false defaults are skipped. */
|
|
20
21
|
merge(defaults: Record<string, AttrValue>): Attributes;
|
|
21
|
-
/**
|
|
22
|
+
/** Adds (or replaces) an attribute at the end. */
|
|
22
23
|
set(key: string, value: AttrValue): Attributes;
|
|
23
24
|
private clone;
|
|
24
|
-
/**
|
|
25
|
+
/** Appends classes after the existing ones; the attribute keeps its position, or goes last. */
|
|
25
26
|
class(list: ClassList): Attributes;
|
|
26
27
|
style(list: (string | false | null | undefined)[]): Attributes;
|
|
27
28
|
when(condition: unknown, fn: (a: Attributes) => Attributes): Attributes;
|
|
28
|
-
/**
|
|
29
|
+
/** Grid container: `fi-grid` classes and `--cols-*` variables per breakpoint. */
|
|
29
30
|
grid(columns?: Record<string, number> | number | null): Attributes;
|
|
30
|
-
/**
|
|
31
|
+
/** Grid column: `fi-grid-col` classes and `--col-span-*` variables per breakpoint. */
|
|
31
32
|
gridColumn(span?: Record<string, number | string> | number | string | null, isHidden?: boolean): Attributes;
|
|
32
|
-
/**
|
|
33
|
+
/** Renders `key="value"` pairs; `true` becomes `key="key"` (or `""` for x-data and wire:*). Values are not escaped again, except `"`. */
|
|
33
34
|
toHtml(): string;
|
|
34
35
|
toString(): string;
|
|
35
36
|
}
|
|
@@ -39,39 +40,39 @@ export declare function evaluate<T, C>(value: Lazy<T, C>, ctx: C): Promise<T>;
|
|
|
39
40
|
export type Palette = Record<number, string>;
|
|
40
41
|
export declare const Color: Record<string, Palette>;
|
|
41
42
|
export declare const WCAG_AA_TEXT = 4.5;
|
|
42
|
-
/**
|
|
43
|
+
/** Converts `#rrggbb`, `rgb(r, g, b)` or `r, g, b` to `oklch(L C h)`, rounded to three decimals. */
|
|
43
44
|
export declare function convertToOklch(color: string): string;
|
|
44
|
-
/**
|
|
45
|
+
/** Builds an eleven-shade palette from a single color, keeping its hue. */
|
|
45
46
|
export declare function generatePalette(color: string): Palette;
|
|
46
|
-
/**
|
|
47
|
+
/** A ready-made palette, or a color (`#6366f1`, `rgb(…)`) from which a palette is generated. */
|
|
47
48
|
export type ColorInput = Palette | string;
|
|
48
49
|
export declare function convertToRgb(color: string): [number, number, number];
|
|
49
|
-
/**
|
|
50
|
+
/** Contrast ratio with luminance approximated by a 2.2 gamma (intentionally not the exact WCAG formula; shade choices depend on it). */
|
|
50
51
|
export declare function calculateContrastRatio(c1: string, c2: string): number;
|
|
51
52
|
export declare function isLight(color: string): boolean;
|
|
52
|
-
/**
|
|
53
|
+
/** Picks accessible background and text shades for a filled button. */
|
|
53
54
|
export declare function buttonColorMap(palette: Palette, minRatio?: number): Record<string, number>;
|
|
54
|
-
/**
|
|
55
|
+
/** First shade of the palette with enough contrast against a surface. */
|
|
55
56
|
export declare function findShade(palette: Palette, surface: string, options?: {
|
|
56
57
|
minRatio?: number;
|
|
57
58
|
maxShade?: number;
|
|
58
59
|
minShade?: number;
|
|
59
60
|
fromDarkest?: boolean;
|
|
60
61
|
}): number | null;
|
|
61
|
-
/**
|
|
62
|
+
/** Color map of badges. */
|
|
62
63
|
export declare function badgeColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
63
|
-
/**
|
|
64
|
+
/** Color map of dropdown items. */
|
|
64
65
|
export declare function dropdownItemColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
65
|
-
/**
|
|
66
|
+
/** Color map of icon columns (non-text contrast, 3:1). */
|
|
66
67
|
export declare function iconColumnColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
67
|
-
/**
|
|
68
|
+
/** Color map of icon buttons (non-text contrast 3:1 on gray-50 / gray-700). */
|
|
68
69
|
export declare function iconButtonColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
69
|
-
/**
|
|
70
|
+
/** Color map of toggles (non-text contrast, 3:1). */
|
|
70
71
|
export declare function toggleColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
71
72
|
export declare function linkColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
72
|
-
/**
|
|
73
|
+
/** Outlined button: only the text color depends on the palette. */
|
|
73
74
|
export declare function outlinedButtonColorMap(palette: Palette, gray: Palette): Record<string, number>;
|
|
74
|
-
/**
|
|
75
|
+
/** Color map of each colored component. */
|
|
75
76
|
declare const COMPONENT_COLOR_MAPS: {
|
|
76
77
|
button: (palette: Palette) => Record<string, number>;
|
|
77
78
|
'button-outlined': typeof outlinedButtonColorMap;
|
|
@@ -92,73 +93,73 @@ declare const COMPONENT_COLOR_MAPS: {
|
|
|
92
93
|
};
|
|
93
94
|
};
|
|
94
95
|
export type ColoredComponent = keyof typeof COMPONENT_COLOR_MAPS;
|
|
95
|
-
/**
|
|
96
|
+
/** Registers named palettes and computes the color classes of components. */
|
|
96
97
|
export declare class ColorManager {
|
|
97
98
|
static DEFAULT_COLORS: Record<string, Palette>;
|
|
98
99
|
private colors;
|
|
99
100
|
private cache;
|
|
100
|
-
/**
|
|
101
|
+
/** Per-request colors (theme picked by the user); they take precedence over the panel's colors. */
|
|
101
102
|
private scope;
|
|
102
103
|
register(colors: Record<string, ColorInput>): this;
|
|
103
|
-
/**
|
|
104
|
+
/** Runs `fn` with colors that only apply to this request; `key` identifies the color set for caching. */
|
|
104
105
|
runWith<T>(key: string, colors: Record<string, ColorInput>, fn: () => T): T;
|
|
105
106
|
getColors(): Record<string, Palette>;
|
|
106
107
|
getColor(name: string): Palette | undefined;
|
|
107
|
-
/**
|
|
108
|
+
/** Color classes of a colored component. */
|
|
108
109
|
getComponentClasses(component: ColoredComponent, color: string | null): string[];
|
|
109
|
-
/**
|
|
110
|
+
/** `<style>` block defining the `--<color>-<shade>` variables, injected into `<head>`. */
|
|
110
111
|
renderCssVariables(): string;
|
|
111
112
|
}
|
|
112
|
-
/**
|
|
113
|
+
/** Heroicons icon (`m-eye`, `o-user`, `s-…`, `c-…`) with the `fi-icon fi-size-<size>` classes. */
|
|
113
114
|
export declare function icon(name: string, options?: {
|
|
114
115
|
size?: string;
|
|
115
116
|
attributes?: Attributes;
|
|
116
117
|
}): string;
|
|
117
|
-
/**
|
|
118
|
+
/** Registers a non-Heroicons icon (brand logo, custom glyph): a full `<svg>` supplied by the application, never by end users (it is not sanitized). */
|
|
118
119
|
export declare function registerIcon(name: string, svg: string): void;
|
|
119
|
-
/**
|
|
120
|
+
/** Loading spinner SVG. */
|
|
120
121
|
export declare function loadingIndicator(attributes?: Attributes, size?: string): string;
|
|
121
|
-
/**
|
|
122
|
+
/** View registry: `filament-forms::components.text-input` resolves to an EJS file; override directories are checked first. */
|
|
122
123
|
export declare class ViewRegistry {
|
|
123
124
|
private namespaces;
|
|
124
125
|
private overrides;
|
|
125
126
|
private compiled;
|
|
126
127
|
private shared;
|
|
127
128
|
namespace(name: string, dir: string): this;
|
|
128
|
-
/**
|
|
129
|
+
/** Application override directory: `<dir>/<namespace>/<path>.ejs` replaces the package view. */
|
|
129
130
|
overrideFrom(dir: string): this;
|
|
130
131
|
share(data: Record<string, unknown>): this;
|
|
131
132
|
resolve(view: string): string;
|
|
132
133
|
render(view: string, data?: Record<string, unknown>): string;
|
|
133
134
|
}
|
|
134
135
|
export declare const views: ViewRegistry;
|
|
135
|
-
/**
|
|
136
|
+
/** Registers a callback applied to every instance of the class (and its subclasses) created afterwards by `make()`. */
|
|
136
137
|
export declare function configureUsing<T>(cls: abstract new (...args: any[]) => T, configure: (instance: T) => void): void;
|
|
137
|
-
/**
|
|
138
|
+
/** Applies the global callbacks to a new instance, from the most general class to the most specific. */
|
|
138
139
|
export declare function configured<T extends object>(instance: T): T;
|
|
139
|
-
/**
|
|
140
|
+
/** Forgets all global callbacks (for tests). */
|
|
140
141
|
export declare function resetConfigurators(): void;
|
|
141
|
-
/**
|
|
142
|
+
/** What a render hook receives: the call's scopes (page, resource…) and the slot's data (`action`, `column`…). */
|
|
142
143
|
export type RenderHookContext = {
|
|
143
144
|
scopes: unknown[];
|
|
144
145
|
data: Record<string, any>;
|
|
145
146
|
};
|
|
146
147
|
export type RenderHookCallback = (context: RenderHookContext) => string | null | undefined;
|
|
147
|
-
/**
|
|
148
|
+
/** Render hook registry. Unscoped hooks render everywhere; scoped hooks only when the call carries one of their scopes. */
|
|
148
149
|
export declare class RenderHookRegistry {
|
|
149
150
|
private hooks;
|
|
150
151
|
register(name: string, hook: RenderHookCallback, scopes?: unknown | unknown[] | null): this;
|
|
151
|
-
/** Hooks
|
|
152
|
+
/** Hooks to render for a call: unscoped first, then scope by scope, each hook once. */
|
|
152
153
|
collect(name: string, scopes: unknown[]): RenderHookCallback[];
|
|
153
154
|
has(name: string, scopes?: unknown | unknown[] | null): boolean;
|
|
154
155
|
render(name: string, scopes?: unknown | unknown[] | null, data?: Record<string, any>): string;
|
|
155
156
|
clear(): void;
|
|
156
157
|
}
|
|
157
|
-
/**
|
|
158
|
+
/** Process-wide hooks (all panels). */
|
|
158
159
|
export declare const renderHooks: RenderHookRegistry;
|
|
159
|
-
/**
|
|
160
|
+
/** Renders with an extra registry (the request panel's) on top of the process-wide hooks. */
|
|
160
161
|
export declare function runWithRenderHooks<T>(registry: RenderHookRegistry, fn: () => T): T;
|
|
161
|
-
/**
|
|
162
|
+
/** Renders a slot: process-wide hooks, then the current panel's hooks. */
|
|
162
163
|
export declare function renderHook(name: string, scopes?: unknown | unknown[] | null, data?: Record<string, any>): string;
|
|
163
164
|
export declare function hasRenderHook(name: string, scopes?: unknown | unknown[] | null): boolean;
|
|
164
165
|
export {};
|
package/lib/index.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
// @navette-js/support —
|
|
3
|
-
//
|
|
2
|
+
// @navette-js/support — shared building blocks: escaping, attribute bags, JS serialization, lazy evaluation, colors, icons,
|
|
3
|
+
// namespaced EJS views.
|
|
4
4
|
import ejs from 'ejs';
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
// ------------------------------------------------------------------
|
|
8
|
+
// ------------------------------------------------------------------ escaping
|
|
9
|
+
/** Escapes `&`, `<`, `>`, `"` and `'` for HTML. */
|
|
9
10
|
export function e(value) {
|
|
10
11
|
return String(value ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
11
12
|
}
|
|
12
|
-
/**
|
|
13
|
+
/** Trusted HTML string: views do not escape it again. */
|
|
13
14
|
export class HtmlString {
|
|
14
15
|
html;
|
|
15
16
|
constructor(html) { this.html = html; }
|
|
16
17
|
toString() { return this.html; }
|
|
17
18
|
}
|
|
18
19
|
export const raw = (html) => new HtmlString(html);
|
|
19
|
-
// ------------------------------------------------------------------
|
|
20
|
-
//
|
|
20
|
+
// ------------------------------------------------------------------ JS serialization
|
|
21
|
+
// Quotes, `<`, `>` and `&` are written as `\uXXXX` so the output is safe inside HTML attributes and `<script>`.
|
|
21
22
|
const BS = String.fromCharCode(92);
|
|
22
23
|
const HEX = { '"': 'u0022', "'": 'u0027', '<': 'u003C', '>': 'u003E', '&': 'u0026', '/': '/' };
|
|
23
24
|
function phpString(value) {
|
|
@@ -34,10 +35,7 @@ function phpString(value) {
|
|
|
34
35
|
}
|
|
35
36
|
return out;
|
|
36
37
|
}
|
|
37
|
-
/**
|
|
38
|
-
* `json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)` : dans les chaînes, `'`, `"`, `<`, `>` et `&`
|
|
39
|
-
* sont écrits en `\uXXXX` (les guillemets de structure restent tels quels), et `/` est échappé.
|
|
40
|
-
*/
|
|
38
|
+
/** JSON where `'`, `"`, `<`, `>` and `&` inside strings become `\uXXXX` (structural quotes are kept) and `/` is escaped. */
|
|
41
39
|
function phpJson(value) {
|
|
42
40
|
const json = JSON.stringify(value);
|
|
43
41
|
const HEX = { "'": BS + 'u0027', '<': BS + 'u003C', '>': BS + 'u003E', '&': BS + 'u0026', '/': BS + '/' };
|
|
@@ -53,7 +51,7 @@ function phpJson(value) {
|
|
|
53
51
|
}
|
|
54
52
|
return out;
|
|
55
53
|
}
|
|
56
|
-
/**
|
|
54
|
+
/** Serializes a value into a JavaScript expression that is safe to embed in HTML. */
|
|
57
55
|
export function js(value) {
|
|
58
56
|
if (value === null || value === undefined)
|
|
59
57
|
return 'null';
|
|
@@ -84,7 +82,7 @@ export class Attributes {
|
|
|
84
82
|
this.map.set(k, v); }
|
|
85
83
|
get(key) { return this.map.get(key); }
|
|
86
84
|
has(key) { return this.map.has(key); }
|
|
87
|
-
/**
|
|
85
|
+
/** Defaults come first, then existing attributes (`class` and `style` are concatenated); null/false defaults are skipped. */
|
|
88
86
|
merge(defaults) {
|
|
89
87
|
const out = new Attributes();
|
|
90
88
|
for (const [k, v] of Object.entries(defaults)) {
|
|
@@ -99,11 +97,11 @@ export class Attributes {
|
|
|
99
97
|
}
|
|
100
98
|
return out;
|
|
101
99
|
}
|
|
102
|
-
/**
|
|
100
|
+
/** Adds (or replaces) an attribute at the end. */
|
|
103
101
|
set(key, value) { const out = this.clone(); out.map.delete(key); out.map.set(key, value); return out; }
|
|
104
102
|
clone() { const out = new Attributes(); for (const [k, v] of this.map)
|
|
105
103
|
out.map.set(k, v); return out; }
|
|
106
|
-
/**
|
|
104
|
+
/** Appends classes after the existing ones; the attribute keeps its position, or goes last. */
|
|
107
105
|
class(list) {
|
|
108
106
|
const out = this.clone();
|
|
109
107
|
out.map.set('class', [out.map.get('class'), cls(list)].filter((x) => x !== '' && x != null).join(' '));
|
|
@@ -119,7 +117,7 @@ export class Attributes {
|
|
|
119
117
|
return out;
|
|
120
118
|
}
|
|
121
119
|
when(condition, fn) { return condition ? fn(this) : this; }
|
|
122
|
-
/**
|
|
120
|
+
/** Grid container: `fi-grid` classes and `--cols-*` variables per breakpoint. */
|
|
123
121
|
grid(columns = {}) {
|
|
124
122
|
const cols = typeof columns === 'number' ? { lg: columns } : { ...(columns ?? {}) };
|
|
125
123
|
for (const k of Object.keys(cols))
|
|
@@ -131,7 +129,7 @@ export class Attributes {
|
|
|
131
129
|
.class(['fi-grid', ...Object.keys(cols).map((bp) => (bp === 'default' ? (cols[bp] > 1 ? 'fi-grid-cols' : '') : `${bp}:fi-grid-cols`))])
|
|
132
130
|
.style(Object.entries(cols).map(([bp, n]) => `--cols-${v(bp)}: repeat(${n}, minmax(0, 1fr))`));
|
|
133
131
|
}
|
|
134
|
-
/**
|
|
132
|
+
/** Grid column: `fi-grid-col` classes and `--col-span-*` variables per breakpoint. */
|
|
135
133
|
gridColumn(span = {}, isHidden = false) {
|
|
136
134
|
const s = span !== null && typeof span === 'object' ? { ...span } : span ? { lg: span } : {};
|
|
137
135
|
for (const k of Object.keys(s))
|
|
@@ -142,7 +140,7 @@ export class Attributes {
|
|
|
142
140
|
.class(['fi-grid-col', { 'fi-hidden': isHidden || s.default === 'hidden' }, ...Object.keys(s).map((bp) => (bp === 'default' ? '' : `${bp}:fi-grid-col-span`))])
|
|
143
141
|
.style(Object.entries(s).map(([bp, n]) => `--col-span-${v(bp)}: ${n === 'full' ? '1 / -1' : `span ${n} / span ${n}`}`));
|
|
144
142
|
}
|
|
145
|
-
/**
|
|
143
|
+
/** Renders `key="value"` pairs; `true` becomes `key="key"` (or `""` for x-data and wire:*). Values are not escaped again, except `"`. */
|
|
146
144
|
toHtml() {
|
|
147
145
|
const parts = [];
|
|
148
146
|
for (const [k, v] of this.map) {
|
|
@@ -159,11 +157,11 @@ export const attrs = (initial = {}) => new Attributes(initial);
|
|
|
159
157
|
export async function evaluate(value, ctx) {
|
|
160
158
|
return typeof value === 'function' ? await value(ctx) : value;
|
|
161
159
|
}
|
|
162
|
-
// ------------------------------------------------------------------
|
|
160
|
+
// ------------------------------------------------------------------ colors
|
|
163
161
|
const here = (p) => fileURLToPath(new URL(p, import.meta.url));
|
|
164
162
|
export const Color = JSON.parse(readFileSync(here('../data/palettes.json'), 'utf8'));
|
|
165
163
|
export const WCAG_AA_TEXT = 4.5;
|
|
166
|
-
/**
|
|
164
|
+
/** Converts `#rrggbb`, `rgb(r, g, b)` or `r, g, b` to `oklch(L C h)`, rounded to three decimals. */
|
|
167
165
|
export function convertToOklch(color) {
|
|
168
166
|
if (color.startsWith('oklch('))
|
|
169
167
|
return color;
|
|
@@ -173,7 +171,7 @@ export function convertToOklch(color) {
|
|
|
173
171
|
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
174
172
|
});
|
|
175
173
|
if (![r, g, b].every(Number.isFinite))
|
|
176
|
-
throw new Error(`
|
|
174
|
+
throw new Error(`Unsupported color: ${color}`);
|
|
177
175
|
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b), m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b), s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
178
176
|
const lightness = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
179
177
|
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, bb = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
@@ -183,14 +181,14 @@ export function convertToOklch(color) {
|
|
|
183
181
|
const round = (n) => String(Math.round(n * 1000) / 1000);
|
|
184
182
|
return `oklch(${round(lightness)} ${round(Math.sqrt(a * a + bb * bb))} ${round(hue)})`;
|
|
185
183
|
}
|
|
186
|
-
/**
|
|
184
|
+
/** Lightness and chroma of each shade of a generated palette. */
|
|
187
185
|
const GENERATED_SHADES = [
|
|
188
186
|
[50, '0.97717647058824', '0.01395454545455'], [100, '0.95035294117647', '0.03272727272727'], [200, '0.90547058823529', '0.06318181818182'],
|
|
189
187
|
[300, '0.84047058823529', '0.10604545454546'], [400, '0.75352941176471', '0.15027272727273'], [500, '0.68270588235294', '0.17009090909091'],
|
|
190
188
|
[600, '0.59782352941176', '0.16913636363636'], [700, '0.51494117647059', '0.14940909090909'], [800, '0.44611764705882', '0.12331818181818'],
|
|
191
189
|
[900, '0.39458823529412', '0.09963636363636'], [950, '0.27788235294118', '0.07136363636364'],
|
|
192
190
|
];
|
|
193
|
-
/**
|
|
191
|
+
/** Builds an eleven-shade palette from a single color, keeping its hue. */
|
|
194
192
|
export function generatePalette(color) {
|
|
195
193
|
const [, , chroma, hue] = /^oklch\(([\d.]+) ([\d.]+) ([\d.]+)\)$/.exec(convertToOklch(color)).map(Number);
|
|
196
194
|
const achromatic = chroma < 0.03;
|
|
@@ -207,14 +205,14 @@ export function convertToRgb(color) {
|
|
|
207
205
|
}
|
|
208
206
|
const m = /^oklch\(([\d.]+)\s+([\d.]+)(?:\s+([\d.]+))?\)/.exec(color);
|
|
209
207
|
if (!m)
|
|
210
|
-
throw new Error(`
|
|
208
|
+
throw new Error(`Unsupported color: ${color}`);
|
|
211
209
|
const L = +m[1], C = +m[2], h = ((+m[3] || 0) * Math.PI) / 180;
|
|
212
210
|
const a = C * Math.cos(h), b = C * Math.sin(h);
|
|
213
211
|
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3, md = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3, s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
214
212
|
const lin = [4.0767416621 * l - 3.3077115913 * md + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * md - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * md + 1.707614701 * s];
|
|
215
213
|
return lin.map((x) => Math.max(0, Math.min(255, Math.round((x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055) * 255))));
|
|
216
214
|
}
|
|
217
|
-
/**
|
|
215
|
+
/** Contrast ratio with luminance approximated by a 2.2 gamma (intentionally not the exact WCAG formula; shade choices depend on it). */
|
|
218
216
|
export function calculateContrastRatio(c1, c2) {
|
|
219
217
|
const lum = (c) => { const [r, g, b] = convertToRgb(c); return 0.2126 * (r / 255) ** 2.2 + 0.7152 * (g / 255) ** 2.2 + 0.0722 * (b / 255) ** 2.2; };
|
|
220
218
|
const a = lum(c1), b = lum(c2);
|
|
@@ -223,10 +221,10 @@ export function calculateContrastRatio(c1, c2) {
|
|
|
223
221
|
export function isLight(color) {
|
|
224
222
|
const m = /^oklch\(([\d.]+)/.exec(color);
|
|
225
223
|
if (!m)
|
|
226
|
-
throw new Error(`isLight
|
|
224
|
+
throw new Error(`isLight: only oklch colors are supported (${color})`);
|
|
227
225
|
return +m[1] >= 0.65;
|
|
228
226
|
}
|
|
229
|
-
/**
|
|
227
|
+
/** Picks accessible background and text shades for a filled button. */
|
|
230
228
|
export function buttonColorMap(palette, minRatio = WCAG_AA_TEXT) {
|
|
231
229
|
const light = [{ bg: 600, hover: 500, alternateHover: null }, { bg: 400, hover: 300, alternateHover: 500 }];
|
|
232
230
|
const dark = [{ bg: 600, hover: 500, alternateHover: 700 }];
|
|
@@ -275,7 +273,7 @@ export function buttonColorMap(palette, minRatio = WCAG_AA_TEXT) {
|
|
|
275
273
|
const [bg, hoverBg] = pick(light), [darkBg, darkHoverBg] = pick(dark);
|
|
276
274
|
return { bg, 'hover:bg': hoverBg, 'dark:bg': darkBg, 'dark:hover:bg': darkHoverBg, text: textForBg[bg] ?? 0, 'hover:text': textForBg[hoverBg] ?? 0, 'dark:text': textForBg[darkBg] ?? 0, 'dark:hover:text': textForBg[darkHoverBg] ?? 0 };
|
|
277
275
|
}
|
|
278
|
-
/**
|
|
276
|
+
/** First shade of the palette with enough contrast against a surface. */
|
|
279
277
|
export function findShade(palette, surface, options = {}) {
|
|
280
278
|
const shades = Object.keys(palette).map(Number).sort((a, b) => (options.fromDarkest ? b - a : a - b));
|
|
281
279
|
for (const shade of shades) {
|
|
@@ -288,11 +286,11 @@ export function findShade(palette, surface, options = {}) {
|
|
|
288
286
|
}
|
|
289
287
|
return null;
|
|
290
288
|
}
|
|
291
|
-
/**
|
|
289
|
+
/** Color map of badges. */
|
|
292
290
|
export function badgeColorMap(palette, gray) {
|
|
293
291
|
return { text: findShade(palette, palette[50]) ?? 900, 'dark:text': findShade(palette, gray[600], { maxShade: 500, fromDarkest: true }) ?? 200 };
|
|
294
292
|
}
|
|
295
|
-
/**
|
|
293
|
+
/** Color map of dropdown items. */
|
|
296
294
|
export function dropdownItemColorMap(palette, gray) {
|
|
297
295
|
return {
|
|
298
296
|
text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950,
|
|
@@ -301,30 +299,30 @@ export function dropdownItemColorMap(palette, gray) {
|
|
|
301
299
|
'dark:hover:text': findShade(palette, palette[950], { maxShade: 400, fromDarkest: true }) ?? 100,
|
|
302
300
|
};
|
|
303
301
|
}
|
|
304
|
-
/**
|
|
302
|
+
/** Color map of icon columns (non-text contrast, 3:1). */
|
|
305
303
|
export function iconColumnColorMap(palette, gray) {
|
|
306
304
|
return { text: findShade(palette, 'oklch(1 0 0)', { minRatio: 3 }) ?? 900, 'dark:text': findShade(palette, gray[800], { minRatio: 3, maxShade: 600, fromDarkest: true }) ?? 200 };
|
|
307
305
|
}
|
|
308
|
-
/**
|
|
306
|
+
/** Color map of icon buttons (non-text contrast 3:1 on gray-50 / gray-700). */
|
|
309
307
|
export function iconButtonColorMap(palette, gray) {
|
|
310
308
|
const light = findShade(palette, gray[50], { minRatio: 3 });
|
|
311
309
|
const dark = findShade(palette, gray[700], { minRatio: 3, maxShade: 500, fromDarkest: true });
|
|
312
|
-
//
|
|
310
|
+
// Above 500 the shade is already dark: hover darkens it instead of lightening it.
|
|
313
311
|
const [text, hoverText] = light === null ? [900, 800] : light > 500 ? [light, light + 100] : [light + 100, light];
|
|
314
312
|
return { text, 'hover:text': hoverText, 'dark:text': dark ?? 200, 'dark:hover:text': dark === null ? 100 : dark - 100 };
|
|
315
313
|
}
|
|
316
|
-
/**
|
|
314
|
+
/** Color map of toggles (non-text contrast, 3:1). */
|
|
317
315
|
export function toggleColorMap(palette, gray) {
|
|
318
316
|
return { bg: findShade(palette, gray[50], { minRatio: 3 }) ?? 900, text: findShade(palette, 'oklch(1 0 0)', { minRatio: 3 }) ?? 900, 'dark:bg': findShade(palette, gray[700], { minRatio: 3, fromDarkest: true }) ?? 200 };
|
|
319
317
|
}
|
|
320
318
|
export function linkColorMap(palette, gray) {
|
|
321
319
|
return { text: findShade(palette, gray[50]) ?? 900, 'dark:text': findShade(palette, gray[700], { maxShade: 400, fromDarkest: true }) ?? 200 };
|
|
322
320
|
}
|
|
323
|
-
/**
|
|
321
|
+
/** Outlined button: only the text color depends on the palette. */
|
|
324
322
|
export function outlinedButtonColorMap(palette, gray) {
|
|
325
323
|
return { text: findShade(palette, gray[50]) ?? 900, 'dark:text': findShade(palette, gray[700], { maxShade: 500, fromDarkest: true }) ?? 200 };
|
|
326
324
|
}
|
|
327
|
-
/**
|
|
325
|
+
/** Color map of each colored component. */
|
|
328
326
|
const COMPONENT_COLOR_MAPS = {
|
|
329
327
|
button: (palette) => buttonColorMap(palette),
|
|
330
328
|
'button-outlined': outlinedButtonColorMap,
|
|
@@ -334,32 +332,32 @@ const COMPONENT_COLOR_MAPS = {
|
|
|
334
332
|
'icon-column': iconColumnColorMap,
|
|
335
333
|
'icon-button': iconButtonColorMap,
|
|
336
334
|
toggle: toggleColorMap,
|
|
337
|
-
//
|
|
335
|
+
// Stat description and stat chart (the chart only gets `fi-color`).
|
|
338
336
|
'stat-description': (palette, gray) => ({ text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950, 'dark:text': findShade(palette, gray[900], { maxShade: 400, fromDarkest: true }) ?? 200 }),
|
|
339
337
|
'stat-chart': () => ({}),
|
|
340
|
-
//
|
|
338
|
+
// Colored text of a column or entry: fixed 500 / 400 for gray, otherwise the same contrast search as stat descriptions.
|
|
341
339
|
text: (palette, gray) => (palette === gray ? { text: 500, 'dark:text': 400 } : { text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950, 'dark:text': findShade(palette, gray[900], { maxShade: 400, fromDarkest: true }) ?? 50 }),
|
|
342
340
|
};
|
|
343
341
|
const CLASS_FOR = {
|
|
344
342
|
bg: (s) => `fi-bg-color-${s}`, 'hover:bg': (s) => `hover:fi-bg-color-${s}`, 'dark:bg': (s) => `dark:fi-bg-color-${s}`, 'dark:hover:bg': (s) => `dark:hover:fi-bg-color-${s}`,
|
|
345
343
|
text: (s) => `fi-text-color-${s}`, 'hover:text': (s) => `hover:fi-text-color-${s}`, 'dark:text': (s) => `dark:fi-text-color-${s}`, 'dark:hover:text': (s) => `dark:hover:fi-text-color-${s}`,
|
|
346
344
|
};
|
|
347
|
-
/**
|
|
345
|
+
/** Registers named palettes and computes the color classes of components. */
|
|
348
346
|
export class ColorManager {
|
|
349
347
|
static DEFAULT_COLORS = { danger: Color.Red, gray: Color.Zinc, info: Color.Blue, primary: Color.Amber, success: Color.Green, warning: Color.Amber };
|
|
350
348
|
colors = { ...ColorManager.DEFAULT_COLORS };
|
|
351
349
|
cache = new Map();
|
|
352
|
-
/**
|
|
350
|
+
/** Per-request colors (theme picked by the user); they take precedence over the panel's colors. */
|
|
353
351
|
scope = new AsyncLocalStorage();
|
|
354
352
|
register(colors) { for (const [name, color] of Object.entries(colors))
|
|
355
353
|
this.colors[name] = toPalette(color); this.cache.clear(); return this; }
|
|
356
|
-
/**
|
|
354
|
+
/** Runs `fn` with colors that only apply to this request; `key` identifies the color set for caching. */
|
|
357
355
|
runWith(key, colors, fn) {
|
|
358
356
|
return this.scope.run({ key, colors: Object.fromEntries(Object.entries(colors).map(([name, color]) => [name, toPalette(color)])) }, fn);
|
|
359
357
|
}
|
|
360
358
|
getColors() { return { ...this.colors, ...(this.scope.getStore()?.colors ?? {}) }; }
|
|
361
359
|
getColor(name) { return this.scope.getStore()?.colors[name] ?? this.colors[name]; }
|
|
362
|
-
/**
|
|
360
|
+
/** Color classes of a colored component. */
|
|
363
361
|
getComponentClasses(component, color) {
|
|
364
362
|
if (!color)
|
|
365
363
|
return [];
|
|
@@ -368,7 +366,7 @@ export class ColorManager {
|
|
|
368
366
|
if (hit)
|
|
369
367
|
return hit;
|
|
370
368
|
let classes = [];
|
|
371
|
-
//
|
|
369
|
+
// Gray is the button's default color and gets no color classes; other components still get them.
|
|
372
370
|
if (color !== 'gray' || component !== 'button') {
|
|
373
371
|
classes = ['fi-color', 'fi-color-' + color.replace(/[^a-zA-Z0-9_-]/g, '')];
|
|
374
372
|
const palette = this.getColor(color);
|
|
@@ -381,47 +379,47 @@ export class ColorManager {
|
|
|
381
379
|
this.cache.set(key, classes);
|
|
382
380
|
return classes;
|
|
383
381
|
}
|
|
384
|
-
/**
|
|
382
|
+
/** `<style>` block defining the `--<color>-<shade>` variables, injected into `<head>`. */
|
|
385
383
|
renderCssVariables() {
|
|
386
384
|
const vars = Object.entries(this.getColors()).map(([name, palette]) => Object.entries(palette).map(([shade, value]) => ` --${name}-${shade}:${value}; `).join('')).join('');
|
|
387
385
|
return `<style>\n :root {\n ${vars}}\n</style>`;
|
|
388
386
|
}
|
|
389
387
|
}
|
|
390
|
-
// ------------------------------------------------------------------
|
|
388
|
+
// ------------------------------------------------------------------ icons
|
|
391
389
|
const iconCache = new Map();
|
|
392
|
-
/**
|
|
390
|
+
/** Heroicons icon (`m-eye`, `o-user`, `s-…`, `c-…`) with the `fi-icon fi-size-<size>` classes. */
|
|
393
391
|
export function icon(name, options = {}) {
|
|
394
392
|
let svg = iconCache.get(name);
|
|
395
393
|
if (svg === undefined) {
|
|
396
394
|
const file = here(`../icons/heroicons/${name.replace(/^heroicon-/, '')}.svg`);
|
|
397
395
|
if (!existsSync(file))
|
|
398
|
-
throw new Error(`
|
|
396
|
+
throw new Error(`Icon not found: ${name}`);
|
|
399
397
|
svg = readFileSync(file, 'utf8').trim();
|
|
400
398
|
iconCache.set(name, svg);
|
|
401
399
|
}
|
|
402
400
|
const a = (options.attributes ?? new Attributes()).class(['fi-icon', `fi-size-${options.size ?? 'md'}`]);
|
|
403
401
|
return svg.replace(/^<svg/, `<svg ${a.toHtml()}`);
|
|
404
402
|
}
|
|
405
|
-
/**
|
|
403
|
+
/** Registers a non-Heroicons icon (brand logo, custom glyph): a full `<svg>` supplied by the application, never by end users (it is not sanitized). */
|
|
406
404
|
export function registerIcon(name, svg) {
|
|
407
405
|
if (!/^<svg[\s>]/.test(svg.trim()))
|
|
408
|
-
throw new Error(`registerIcon(${name})
|
|
406
|
+
throw new Error(`registerIcon(${name}) expects an <svg> element.`);
|
|
409
407
|
iconCache.set(name, svg.trim());
|
|
410
408
|
}
|
|
411
|
-
/**
|
|
409
|
+
/** Loading spinner SVG. */
|
|
412
410
|
export function loadingIndicator(attributes = new Attributes(), size = 'md') {
|
|
413
411
|
const a = attributes.class(['fi-icon fi-loading-indicator', `fi-size-${size}`]);
|
|
414
412
|
return `<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" ${a.toHtml()}><path clip-rule="evenodd" d="M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill-rule="evenodd" fill="currentColor" opacity="0.2"></path><path d="M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z" fill="currentColor"></path></svg>`;
|
|
415
413
|
}
|
|
416
|
-
// ------------------------------------------------------------------
|
|
417
|
-
/**
|
|
414
|
+
// ------------------------------------------------------------------ namespaced, overridable EJS views
|
|
415
|
+
/** View registry: `filament-forms::components.text-input` resolves to an EJS file; override directories are checked first. */
|
|
418
416
|
export class ViewRegistry {
|
|
419
417
|
namespaces = new Map();
|
|
420
418
|
overrides = [];
|
|
421
419
|
compiled = new Map();
|
|
422
420
|
shared = {};
|
|
423
421
|
namespace(name, dir) { this.namespaces.set(name, dir); return this; }
|
|
424
|
-
/**
|
|
422
|
+
/** Application override directory: `<dir>/<namespace>/<path>.ejs` replaces the package view. */
|
|
425
423
|
overrideFrom(dir) { this.overrides.unshift(dir); this.compiled.clear(); return this; }
|
|
426
424
|
share(data) { Object.assign(this.shared, data); return this; }
|
|
427
425
|
resolve(view) {
|
|
@@ -434,7 +432,7 @@ export class ViewRegistry {
|
|
|
434
432
|
}
|
|
435
433
|
const base = this.namespaces.get(ns);
|
|
436
434
|
if (!base)
|
|
437
|
-
throw new Error(`
|
|
435
|
+
throw new Error(`Unknown view namespace: ${ns}`);
|
|
438
436
|
return path.join(base, rel);
|
|
439
437
|
}
|
|
440
438
|
render(view, data = {}) {
|
|
@@ -448,13 +446,13 @@ export class ViewRegistry {
|
|
|
448
446
|
}
|
|
449
447
|
}
|
|
450
448
|
export const views = new ViewRegistry();
|
|
451
|
-
// ------------------------------------------------------------------
|
|
449
|
+
// ------------------------------------------------------------------ global configuration
|
|
452
450
|
const configurators = new Map();
|
|
453
|
-
/**
|
|
451
|
+
/** Registers a callback applied to every instance of the class (and its subclasses) created afterwards by `make()`. */
|
|
454
452
|
export function configureUsing(cls, configure) {
|
|
455
453
|
configurators.set(cls, [...(configurators.get(cls) ?? []), configure]);
|
|
456
454
|
}
|
|
457
|
-
/**
|
|
455
|
+
/** Applies the global callbacks to a new instance, from the most general class to the most specific. */
|
|
458
456
|
export function configured(instance) {
|
|
459
457
|
if (!configurators.size)
|
|
460
458
|
return instance;
|
|
@@ -466,10 +464,10 @@ export function configured(instance) {
|
|
|
466
464
|
configure(instance);
|
|
467
465
|
return instance;
|
|
468
466
|
}
|
|
469
|
-
/**
|
|
467
|
+
/** Forgets all global callbacks (for tests). */
|
|
470
468
|
export function resetConfigurators() { configurators.clear(); }
|
|
471
469
|
const UNSCOPED = Symbol('unscoped');
|
|
472
|
-
/**
|
|
470
|
+
/** Render hook registry. Unscoped hooks render everywhere; scoped hooks only when the call carries one of their scopes. */
|
|
473
471
|
export class RenderHookRegistry {
|
|
474
472
|
hooks = new Map();
|
|
475
473
|
register(name, hook, scopes = null) {
|
|
@@ -480,7 +478,7 @@ export class RenderHookRegistry {
|
|
|
480
478
|
byScope.set(scope, [...(byScope.get(scope) ?? []), hook]);
|
|
481
479
|
return this;
|
|
482
480
|
}
|
|
483
|
-
/** Hooks
|
|
481
|
+
/** Hooks to render for a call: unscoped first, then scope by scope, each hook once. */
|
|
484
482
|
collect(name, scopes) {
|
|
485
483
|
const byScope = this.hooks.get(name);
|
|
486
484
|
if (!byScope)
|
|
@@ -500,13 +498,13 @@ export class RenderHookRegistry {
|
|
|
500
498
|
clear() { this.hooks.clear(); }
|
|
501
499
|
}
|
|
502
500
|
const toScopes = (scopes) => (scopes === null || scopes === undefined ? [] : Array.isArray(scopes) ? scopes : [scopes]);
|
|
503
|
-
/**
|
|
501
|
+
/** Process-wide hooks (all panels). */
|
|
504
502
|
export const renderHooks = new RenderHookRegistry();
|
|
505
503
|
const renderHookScope = new AsyncLocalStorage();
|
|
506
|
-
/**
|
|
504
|
+
/** Renders with an extra registry (the request panel's) on top of the process-wide hooks. */
|
|
507
505
|
export function runWithRenderHooks(registry, fn) { return renderHookScope.run(registry, fn); }
|
|
508
506
|
const activeRegistries = () => { const own = renderHookScope.getStore(); return own && own !== renderHooks ? [renderHooks, own] : [renderHooks]; };
|
|
509
|
-
/**
|
|
507
|
+
/** Renders a slot: process-wide hooks, then the current panel's hooks. */
|
|
510
508
|
export function renderHook(name, scopes = null, data = {}) {
|
|
511
509
|
return activeRegistries().map((registry) => registry.render(name, scopes, data)).join('');
|
|
512
510
|
}
|
package/lib/ui.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cls, e, js, loadingIndicator, type ColorManager } from './index.ts';
|
|
2
|
-
/**
|
|
2
|
+
/** Sets the color manager used by the components (usually the panel's). */
|
|
3
3
|
export declare const useColorManager: (colors: ColorManager) => void;
|
|
4
4
|
export type Attrs = Record<string, string | number | boolean | null | undefined>;
|
|
5
5
|
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -11,7 +11,7 @@ export type BadgeProps = {
|
|
|
11
11
|
tag?: string;
|
|
12
12
|
attributes?: Attrs;
|
|
13
13
|
};
|
|
14
|
-
/**
|
|
14
|
+
/** Badge. */
|
|
15
15
|
export declare function badge(props?: BadgeProps, slot?: string): string;
|
|
16
16
|
export type IconButtonProps = {
|
|
17
17
|
icon: string;
|
|
@@ -25,7 +25,7 @@ export type IconButtonProps = {
|
|
|
25
25
|
badgeColor?: string;
|
|
26
26
|
attributes?: Attrs;
|
|
27
27
|
};
|
|
28
|
-
/**
|
|
28
|
+
/** Icon-only button; the label is used as tooltip and accessible name. */
|
|
29
29
|
export declare function iconButton(props: IconButtonProps): string;
|
|
30
30
|
export type ButtonProps = {
|
|
31
31
|
color?: string;
|
|
@@ -41,7 +41,7 @@ export type ButtonProps = {
|
|
|
41
41
|
badgeColor?: string;
|
|
42
42
|
attributes?: Attrs;
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
44
|
+
/** Button. */
|
|
45
45
|
export declare function button(props?: ButtonProps, slot?: string): string;
|
|
46
46
|
export type LinkProps = {
|
|
47
47
|
href?: string | null;
|
|
@@ -55,9 +55,9 @@ export type LinkProps = {
|
|
|
55
55
|
badgeColor?: string;
|
|
56
56
|
attributes?: Attrs;
|
|
57
57
|
};
|
|
58
|
-
/**
|
|
58
|
+
/** Link. */
|
|
59
59
|
export declare function link(props?: LinkProps, slot?: string): string;
|
|
60
|
-
/** Avatar
|
|
60
|
+
/** Avatar image, circular by default. */
|
|
61
61
|
export declare function avatar(props: {
|
|
62
62
|
src: string;
|
|
63
63
|
alt?: string;
|
|
@@ -66,20 +66,20 @@ export declare function avatar(props: {
|
|
|
66
66
|
attributes?: Attrs;
|
|
67
67
|
}): string;
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* Breadcrumbs. An item without `url` is plain text; an item with an empty `url` is still a link, without `href`.
|
|
70
|
+
* The last item is the current page.
|
|
71
71
|
*/
|
|
72
72
|
export declare function breadcrumbs(items: {
|
|
73
73
|
label: string;
|
|
74
74
|
url?: string | null;
|
|
75
75
|
}[], label?: string): string;
|
|
76
|
-
/**
|
|
76
|
+
/** Bare checkbox. */
|
|
77
77
|
export declare function checkbox(props?: {
|
|
78
78
|
checked?: boolean;
|
|
79
79
|
valid?: boolean;
|
|
80
80
|
attributes?: Attrs;
|
|
81
81
|
}): string;
|
|
82
|
-
/**
|
|
82
|
+
/** Loading spinner. */
|
|
83
83
|
export declare function spinner(attributes?: Attrs): string;
|
|
84
84
|
export type InputWrapperProps = {
|
|
85
85
|
prefix?: string | null;
|
|
@@ -94,13 +94,13 @@ export type InputWrapperProps = {
|
|
|
94
94
|
inlineSuffix?: boolean;
|
|
95
95
|
attributes?: Attrs;
|
|
96
96
|
};
|
|
97
|
-
/**
|
|
97
|
+
/** Input wrapper: border, prefix and suffix (text or icon). */
|
|
98
98
|
export declare function inputWrapper(props?: InputWrapperProps, slot?: string): string;
|
|
99
|
-
/**
|
|
99
|
+
/** Bare text input, meant to be placed in `inputWrapper`. */
|
|
100
100
|
export declare function input(attributes?: Attrs): string;
|
|
101
|
-
/**
|
|
101
|
+
/** Bare native select; `slot` holds the `<option>` elements. */
|
|
102
102
|
export declare function select(attributes?: Attrs, slot?: string): string;
|
|
103
|
-
/**
|
|
103
|
+
/** Fieldset with an optional legend. */
|
|
104
104
|
export declare function fieldset(props?: {
|
|
105
105
|
label?: string | null;
|
|
106
106
|
labelHidden?: boolean;
|
|
@@ -117,7 +117,7 @@ export type CalloutProps = {
|
|
|
117
117
|
controls?: string | null;
|
|
118
118
|
attributes?: Attrs;
|
|
119
119
|
};
|
|
120
|
-
/**
|
|
120
|
+
/** Callout box. `footer` and `controls` take already rendered HTML (buttons…). */
|
|
121
121
|
export declare function callout(props?: CalloutProps): string;
|
|
122
122
|
export type EmptyStateProps = {
|
|
123
123
|
heading: string;
|
|
@@ -129,21 +129,21 @@ export type EmptyStateProps = {
|
|
|
129
129
|
contained?: boolean;
|
|
130
130
|
attributes?: Attrs;
|
|
131
131
|
};
|
|
132
|
-
/**
|
|
132
|
+
/** Empty state: icon, heading, description and footer (already rendered actions). */
|
|
133
133
|
export declare function emptyState(props: EmptyStateProps): string;
|
|
134
134
|
export type PaginationProps = {
|
|
135
|
-
/**
|
|
135
|
+
/** Host component id (prefix of the `wire:key` values). */
|
|
136
136
|
id: string;
|
|
137
137
|
page: number;
|
|
138
138
|
perPage: number;
|
|
139
|
-
/**
|
|
139
|
+
/** Total record count; omit it for simple (previous / next) pagination. */
|
|
140
140
|
total?: number;
|
|
141
141
|
hasMore?: boolean;
|
|
142
142
|
pageOptions?: (number | 'all')[];
|
|
143
143
|
extremeLinks?: boolean;
|
|
144
144
|
property?: string;
|
|
145
145
|
};
|
|
146
|
-
/** Pagination
|
|
146
|
+
/** Pagination bound to the host component's `previousPage`, `nextPage` and `gotoPage` methods. */
|
|
147
147
|
export declare function pagination(props: PaginationProps): string;
|
|
148
148
|
export type SectionProps = {
|
|
149
149
|
heading?: string | null;
|
|
@@ -164,7 +164,7 @@ export type SectionProps = {
|
|
|
164
164
|
secondary?: boolean;
|
|
165
165
|
attributes?: Attrs;
|
|
166
166
|
};
|
|
167
|
-
/**
|
|
167
|
+
/** Collapsible card with an optional header (icon, heading, description, after-header area). */
|
|
168
168
|
export declare function section(props?: SectionProps, slot?: string): string;
|
|
169
169
|
export type TabItem = {
|
|
170
170
|
label: string;
|
|
@@ -177,7 +177,7 @@ export type TabItem = {
|
|
|
177
177
|
href?: string | null;
|
|
178
178
|
attributes?: Attrs;
|
|
179
179
|
};
|
|
180
|
-
/**
|
|
180
|
+
/** Tabs. */
|
|
181
181
|
export declare function tabs(props: {
|
|
182
182
|
label?: string | null;
|
|
183
183
|
vertical?: boolean;
|
|
@@ -197,11 +197,11 @@ export type DropdownItem = {
|
|
|
197
197
|
disabled?: boolean;
|
|
198
198
|
attributes?: Attrs;
|
|
199
199
|
};
|
|
200
|
-
/**
|
|
200
|
+
/** Dropdown menu item. */
|
|
201
201
|
export declare function dropdownItem(item: DropdownItem): string;
|
|
202
|
-
/**
|
|
202
|
+
/** List of dropdown menu items. */
|
|
203
203
|
export declare function dropdownList(items: DropdownItem[]): string;
|
|
204
|
-
/**
|
|
204
|
+
/** Dropdown: `trigger` is the trigger's HTML, `slot` the panel content. */
|
|
205
205
|
export declare function dropdown(props: {
|
|
206
206
|
trigger: string;
|
|
207
207
|
width?: string | null;
|
|
@@ -217,7 +217,7 @@ export type ModalProps = {
|
|
|
217
217
|
eyebrow?: string | null;
|
|
218
218
|
icon?: string | null;
|
|
219
219
|
iconColor?: string;
|
|
220
|
-
/**
|
|
220
|
+
/** Footer buttons (already rendered HTML), or `footer` for a fully custom footer. */
|
|
221
221
|
footerActions?: string[];
|
|
222
222
|
footer?: string | null;
|
|
223
223
|
footerActionsAlignment?: 'start' | 'center' | 'end';
|
|
@@ -230,10 +230,10 @@ export type ModalProps = {
|
|
|
230
230
|
closeButton?: boolean;
|
|
231
231
|
closeByClickingAway?: boolean;
|
|
232
232
|
closeByEscaping?: boolean;
|
|
233
|
-
/**
|
|
233
|
+
/** Host component id, used to prefix `wire:key` values when the modal lives in a stateful component. */
|
|
234
234
|
livewireId?: string | null;
|
|
235
235
|
};
|
|
236
|
-
/**
|
|
236
|
+
/** Modal, opened by an `open-modal` event carrying its id. */
|
|
237
237
|
export declare function modal(props: ModalProps, slot?: string): string;
|
|
238
238
|
export declare const ui: {
|
|
239
239
|
avatar: typeof avatar;
|
package/lib/ui.js
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
// @navette-js/support/ui —
|
|
2
|
-
//
|
|
3
|
-
// (sources/filament/packages/support/resources/views/components) ; elles sont vérifiées scène par scène contre l'application
|
|
4
|
-
// de documentation de Filament (`node tools/parity/run-scenes.mjs`).
|
|
1
|
+
// @navette-js/support/ui — base UI components (button, badge, modal…) usable on their own, outside a panel.
|
|
2
|
+
// Each function returns an HTML string.
|
|
5
3
|
import { Attributes, cls, e, icon, js, loadingIndicator, views } from "./index.js";
|
|
6
4
|
import { t, tc } from "./i18n.js";
|
|
7
5
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
//
|
|
6
|
+
// The base views (`filament::components.*`) live in this package so they work without a panel.
|
|
9
7
|
views.namespace('filament', fileURLToPath(new URL('../views', import.meta.url)));
|
|
10
8
|
let manager = null;
|
|
11
|
-
/**
|
|
9
|
+
/** Sets the color manager used by the components (usually the panel's). */
|
|
12
10
|
export const useColorManager = (colors) => { manager = colors; };
|
|
13
11
|
const colorsOf = (component, color) => {
|
|
14
12
|
if (!manager)
|
|
15
|
-
throw new Error('
|
|
13
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
16
14
|
return color && color !== 'gray' ? manager.getComponentClasses(component, color) : [];
|
|
17
15
|
};
|
|
18
|
-
/**
|
|
16
|
+
/** Badge. */
|
|
19
17
|
export function badge(props = {}, slot = '') {
|
|
20
18
|
const { color = 'primary', size = 'md', tag = 'span' } = props;
|
|
21
19
|
const attributes = new Attributes(props.attributes ?? {}).class([...colorsOf('badge', color), 'fi-badge', `fi-size-${size}`]);
|
|
22
20
|
const iconHtml = props.icon ? icon(props.icon, { size: 'sm' }) : '';
|
|
23
21
|
return `<${tag} ${attributes.toHtml()}>${props.iconPosition === 'after' ? '' : iconHtml}<span class="fi-badge-label-ctn"><span class="fi-badge-label">${slot}</span></span>${props.iconPosition === 'after' ? iconHtml : ''}</${tag}>`;
|
|
24
22
|
}
|
|
25
|
-
/**
|
|
23
|
+
/** Small counter on a button or link (rendered without the badge label wrapper). */
|
|
26
24
|
const counter = (value, color = 'primary') => `<span class="${cls([...colorsOf('badge', color), 'fi-badge', 'fi-size-xs'])}">${e(String(value))}</span>`;
|
|
27
|
-
/** Taille de l'icône selon celle du bouton (comme la vue Blade). */
|
|
28
25
|
const ICON_BUTTON_ICON_SIZE = { xs: 'sm', sm: 'md', md: 'md', lg: 'lg', xl: 'lg' };
|
|
29
|
-
/**
|
|
26
|
+
/** Icon-only button; the label is used as tooltip and accessible name. */
|
|
30
27
|
export function iconButton(props) {
|
|
31
28
|
const { color = 'primary', size = 'md', disabled = false, tag = 'button' } = props;
|
|
32
|
-
//
|
|
29
|
+
// The caller's classes go after the component's own classes.
|
|
33
30
|
const { class: extraClass = null, ...rest } = (props.attributes ?? {});
|
|
34
31
|
const attributes = new Attributes(rest)
|
|
35
32
|
.merge({
|
|
@@ -40,7 +37,7 @@ export function iconButton(props) {
|
|
|
40
37
|
const badgeHtml = props.badge !== null && props.badge !== undefined ? `<div class="fi-icon-btn-badge-ctn">${counter(props.badge, props.badgeColor)}</div>` : '';
|
|
41
38
|
return `<${tag} ${attributes.toHtml()}>${icon(props.icon, { size: ICON_BUTTON_ICON_SIZE[size] })}${badgeHtml}</${tag}>`;
|
|
42
39
|
}
|
|
43
|
-
/**
|
|
40
|
+
/** Button. */
|
|
44
41
|
export function button(props = {}, slot = '') {
|
|
45
42
|
const { color = 'primary', size = 'md', outlined = false, disabled = false, tag = 'button', type = 'button', iconPosition = 'before' } = props;
|
|
46
43
|
const attributes = new Attributes(props.attributes ?? {})
|
|
@@ -53,7 +50,7 @@ export function button(props = {}, slot = '') {
|
|
|
53
50
|
const badgeHtml = props.badge !== null && props.badge !== undefined ? `<div class="fi-btn-badge-ctn">${counter(props.badge, props.badgeColor)}</div>` : '';
|
|
54
51
|
return `<${tag} ${attributes.toHtml()}>${iconPosition === 'before' ? iconHtml : ''}${slot}${iconPosition === 'after' ? iconHtml : ''}${badgeHtml}</${tag}>`;
|
|
55
52
|
}
|
|
56
|
-
/**
|
|
53
|
+
/** Link. */
|
|
57
54
|
export function link(props = {}, slot = '') {
|
|
58
55
|
const { color = 'primary', size = 'md', tag = 'a', iconPosition = 'before' } = props;
|
|
59
56
|
const attributes = new Attributes(props.attributes ?? {})
|
|
@@ -64,37 +61,37 @@ export function link(props = {}, slot = '') {
|
|
|
64
61
|
return `<${tag} ${attributes.toHtml()}>${iconPosition === 'before' ? iconHtml : ''}<span class="fi-link-label">${slot}</span>${iconPosition === 'after' ? iconHtml : ''}${badgeHtml}</${tag}>`;
|
|
65
62
|
}
|
|
66
63
|
// ------------------------------------------------------------------ avatar
|
|
67
|
-
/** Avatar
|
|
64
|
+
/** Avatar image, circular by default. */
|
|
68
65
|
export function avatar(props) {
|
|
69
66
|
const attributes = new Attributes(props.attributes ?? {}).merge({ alt: props.alt ?? null, src: props.src }).class(['fi-avatar', { 'fi-circular': props.circular !== false }, `fi-size-${props.size ?? 'md'}`]);
|
|
70
67
|
return `<img ${attributes.toHtml()} />`;
|
|
71
68
|
}
|
|
72
69
|
// ------------------------------------------------------------------ breadcrumbs
|
|
73
70
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
71
|
+
* Breadcrumbs. An item without `url` is plain text; an item with an empty `url` is still a link, without `href`.
|
|
72
|
+
* The last item is the current page.
|
|
76
73
|
*/
|
|
77
74
|
export function breadcrumbs(items, label = 'Breadcrumb') {
|
|
78
75
|
const separators = icon('m-chevron-right', { attributes: new Attributes({ 'aria-hidden': 'true', class: 'fi-breadcrumbs-item-separator fi-ltr' }) }) + icon('m-chevron-left', { attributes: new Attributes({ 'aria-hidden': 'true', class: 'fi-breadcrumbs-item-separator fi-rtl' }) });
|
|
79
76
|
const list = items.map((item, index) => `<li class="fi-breadcrumbs-item">${index > 0 ? separators : ''}${((current) => (item.url === undefined || item.url === null ? `<span${current} class="fi-breadcrumbs-item-label">${e(item.label)}</span>` : `<a${item.url ? ` href="${e(item.url)}"` : ''}${current} class="fi-breadcrumbs-item-label">${e(item.label)}</a>`))(index === items.length - 1 ? ' aria-current="page"' : '')}</li>`);
|
|
80
77
|
return `<nav class="fi-breadcrumbs" aria-label="${e(label)}"><ol class="fi-breadcrumbs-list">${list.join('')}</ol></nav>`;
|
|
81
78
|
}
|
|
82
|
-
// ------------------------------------------------------------------
|
|
83
|
-
/**
|
|
79
|
+
// ------------------------------------------------------------------ basic inputs
|
|
80
|
+
/** Bare checkbox. */
|
|
84
81
|
export function checkbox(props = {}) {
|
|
85
82
|
const attributes = new Attributes(props.attributes ?? {}).merge({ type: 'checkbox', checked: props.checked === true }).class(['fi-checkbox-input', props.valid === false ? 'fi-invalid' : 'fi-valid']);
|
|
86
83
|
return `<input ${attributes.toHtml()} />`;
|
|
87
84
|
}
|
|
88
|
-
/**
|
|
85
|
+
/** Loading spinner. */
|
|
89
86
|
export function spinner(attributes = {}) {
|
|
90
|
-
//
|
|
87
|
+
// The caller's classes go after the component's own classes.
|
|
91
88
|
const { class: extra, ...rest } = attributes;
|
|
92
89
|
const html = loadingIndicator(new Attributes(rest));
|
|
93
90
|
return extra ? html.replace(/class="([^"]*)"/, (_, own) => `class="${own} ${e(String(extra))}"`) : html;
|
|
94
91
|
}
|
|
95
92
|
const FOCUS_INPUT = "if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')";
|
|
96
93
|
const affixIcon = (name, color) => icon(name, { attributes: new Attributes().class(color && color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) });
|
|
97
|
-
/**
|
|
94
|
+
/** Input wrapper: border, prefix and suffix (text or icon). */
|
|
98
95
|
export function inputWrapper(props = {}, slot = '') {
|
|
99
96
|
const hasPrefix = !!(props.prefix || props.prefixIcon), hasSuffix = !!(props.suffix || props.suffixIcon);
|
|
100
97
|
const attributes = new Attributes(props.attributes ?? {}).class(['fi-input-wrp', { 'fi-disabled': props.disabled === true, 'fi-invalid': props.valid === false }]);
|
|
@@ -102,31 +99,31 @@ export function inputWrapper(props = {}, slot = '') {
|
|
|
102
99
|
const suffix = hasSuffix ? `<div x-on:click="${FOCUS_INPUT}" class="${cls(['fi-input-wrp-suffix', { 'fi-inline': props.inlineSuffix === true, 'fi-input-wrp-suffix-has-label': !!props.suffix }])}">${props.suffix ? `<span class="fi-input-wrp-label">${e(props.suffix)}</span>` : ''}${props.suffixIcon ? affixIcon(props.suffixIcon, props.suffixIconColor) : ''}</div>` : '';
|
|
103
100
|
return `<div ${attributes.toHtml()}>${prefix}<div class="fi-input-wrp-content-ctn">${slot}</div>${suffix}</div>`;
|
|
104
101
|
}
|
|
105
|
-
/**
|
|
102
|
+
/** Bare text input, meant to be placed in `inputWrapper`. */
|
|
106
103
|
export function input(attributes = {}) { return `<input ${new Attributes(attributes).class(['fi-input']).toHtml()} />`; }
|
|
107
|
-
/**
|
|
104
|
+
/** Bare native select; `slot` holds the `<option>` elements. */
|
|
108
105
|
export function select(attributes = {}, slot = '') { return `<select ${new Attributes(attributes).class(['fi-select-input']).toHtml()}>${slot}</select>`; }
|
|
109
|
-
/**
|
|
106
|
+
/** Fieldset with an optional legend. */
|
|
110
107
|
export function fieldset(props = {}, slot = '') {
|
|
111
108
|
const attributes = new Attributes(props.attributes ?? {}).class(['fi-fieldset', { 'fi-fieldset-label-hidden': props.labelHidden === true }]);
|
|
112
109
|
return `<fieldset ${attributes.toHtml()}>${props.label ? `<legend>${e(props.label)}</legend>` : ''}${slot}</fieldset>`;
|
|
113
110
|
}
|
|
114
111
|
const colorMark = (color) => (color && color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []);
|
|
115
|
-
/**
|
|
112
|
+
/** Callout box. `footer` and `controls` take already rendered HTML (buttons…). */
|
|
116
113
|
export function callout(props = {}) {
|
|
117
114
|
const attributes = new Attributes(props.attributes ?? {}).class(['fi-callout', ...colorMark(props.color)]);
|
|
118
115
|
const iconHtml = props.icon ? icon(props.icon, { size: props.iconSize ?? 'lg', attributes: new Attributes().class([...colorMark(props.iconColor ?? props.color), 'fi-callout-icon']) }) : '';
|
|
119
116
|
const text = `<div class="fi-callout-text">${props.heading ? `<h4 class="fi-callout-heading">${e(props.heading)}</h4>` : ''}${props.description ? `<p class="fi-callout-description">${e(props.description)}</p>` : ''}</div>`;
|
|
120
117
|
return `<div ${attributes.toHtml()}>${iconHtml}<div class="fi-callout-main">${text}${props.footer ? `<div class="fi-callout-footer">${props.footer}</div>` : ''}</div>${props.controls ? `<div class="fi-callout-controls">${props.controls}</div>` : ''}</div>`;
|
|
121
118
|
}
|
|
122
|
-
/**
|
|
119
|
+
/** Empty state: icon, heading, description and footer (already rendered actions). */
|
|
123
120
|
export function emptyState(props) {
|
|
124
121
|
const color = props.iconColor ?? 'primary';
|
|
125
122
|
const attributes = new Attributes(props.attributes ?? {}).class(['fi-empty-state', { 'fi-empty-state-not-contained': props.contained === false }]);
|
|
126
123
|
const iconHtml = props.icon ? `<div class="${cls(['fi-empty-state-icon-bg', ...colorMark(color)])}">${icon(props.icon, { size: props.iconSize ?? 'lg', attributes: new Attributes().class(colorMark(color)) })}</div>` : '';
|
|
127
124
|
return `<div ${attributes.toHtml()}><div class="fi-empty-state-content">${iconHtml}<div class="fi-empty-state-text-ctn"><h2 class="fi-empty-state-heading">${e(props.heading)}</h2>${props.description ? `<p class="fi-empty-state-description">${e(props.description)}</p>` : ''}${props.footer ? `<footer class="fi-empty-state-footer">${props.footer}</footer>` : ''}</div></div></div>`;
|
|
128
125
|
}
|
|
129
|
-
/**
|
|
126
|
+
/** Page numbers to show: all of them up to eleven pages, otherwise a window around the current page. */
|
|
130
127
|
function pageElements(current, last) {
|
|
131
128
|
if (last <= 11)
|
|
132
129
|
return Array.from({ length: last }, (_, i) => i + 1);
|
|
@@ -139,7 +136,7 @@ function pageElements(current, last) {
|
|
|
139
136
|
}
|
|
140
137
|
return out;
|
|
141
138
|
}
|
|
142
|
-
/** Pagination
|
|
139
|
+
/** Pagination bound to the host component's `previousPage`, `nextPage` and `gotoPage` methods. */
|
|
143
140
|
export function pagination(props) {
|
|
144
141
|
const simple = props.total === undefined;
|
|
145
142
|
const lastPage = simple ? props.page + (props.hasMore ? 1 : 0) : Math.max(1, Math.ceil(props.total / props.perPage));
|
|
@@ -153,7 +150,7 @@ export function pagination(props) {
|
|
|
153
150
|
attrs: (o) => new Attributes(o),
|
|
154
151
|
});
|
|
155
152
|
}
|
|
156
|
-
/**
|
|
153
|
+
/** Collapsible card with an optional header (icon, heading, description, after-header area). */
|
|
157
154
|
export function section(props = {}, slot = '') {
|
|
158
155
|
const collapsible = props.collapsible === true, collapsed = collapsible && props.collapsed === true;
|
|
159
156
|
const hasText = !!props.heading || !!props.description;
|
|
@@ -187,7 +184,7 @@ export function section(props = {}, slot = '') {
|
|
|
187
184
|
const body = slot !== '' || props.footer ? `<div${collapsible ? ` x-bind:id="$id('fi-section-content')"${collapsed ? ' x-cloak' : ''}` : ''} class="fi-section-content-ctn">${slot !== '' ? `<div class="fi-section-content">${slot}</div>` : ''}${props.footer ? `<footer class="fi-section-footer">${props.footer}</footer>` : ''}</div>` : '';
|
|
188
185
|
return `<section ${attributes.toHtml()}>${header}${body}</section>`;
|
|
189
186
|
}
|
|
190
|
-
/**
|
|
187
|
+
/** Tabs. */
|
|
191
188
|
export function tabs(props, items) {
|
|
192
189
|
const attributes = new Attributes(props.attributes ?? {}).merge({ 'aria-label': props.label ?? null }).class(['fi-tabs', { 'fi-contained': props.contained === true, 'fi-vertical': props.vertical === true }]);
|
|
193
190
|
const html = items.map((item) => {
|
|
@@ -201,7 +198,7 @@ export function tabs(props, items) {
|
|
|
201
198
|
});
|
|
202
199
|
return `<nav ${attributes.toHtml()}>${html.join('')}</nav>`;
|
|
203
200
|
}
|
|
204
|
-
/**
|
|
201
|
+
/** Dropdown menu item. */
|
|
205
202
|
export function dropdownItem(item) {
|
|
206
203
|
const tag = item.tag ?? 'button';
|
|
207
204
|
const color = item.color ?? 'gray';
|
|
@@ -216,15 +213,15 @@ export function dropdownItem(item) {
|
|
|
216
213
|
const badgeHtml = item.badge !== null && item.badge !== undefined ? `<span class="${cls([...colorsOf('badge', item.badgeColor ?? 'primary'), 'fi-badge'])}">${e(String(item.badge))}</span>` : '';
|
|
217
214
|
return `<${tag} ${attributes.toHtml()}>${lead}<span class="fi-dropdown-list-item-label">${e(item.label)}</span>${badgeHtml}</${tag}>`;
|
|
218
215
|
}
|
|
219
|
-
/**
|
|
216
|
+
/** List of dropdown menu items. */
|
|
220
217
|
export function dropdownList(items) { return `<div class="fi-dropdown-list">${items.map(dropdownItem).join('')}</div>`; }
|
|
221
|
-
/**
|
|
218
|
+
/** Dropdown: `trigger` is the trigger's HTML, `slot` the panel content. */
|
|
222
219
|
export function dropdown(props, slot = '') {
|
|
223
220
|
const attributes = new Attributes(props.attributes ?? {}).merge({ 'x-data': 'filamentDropdown({ isMenu: true })' }).class(['fi-dropdown']);
|
|
224
221
|
const float = `x-float${props.placement ? `.placement.${props.placement}` : ''}.flip.offset`;
|
|
225
222
|
return `<div ${attributes.toHtml()}><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger">${props.trigger}</div><div x-cloak ${float}="{ offset: ${props.offset ?? 8}, }" x-ref="panel" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" role="menu" class="${cls(['fi-dropdown-panel', props.width ? `fi-width-${props.width}` : null, { 'fi-scrollable': !!props.maxHeight }])}" style="${props.maxHeight ? `max-height: ${e(props.maxHeight)};` : ''}">${slot}</div></div>`;
|
|
226
223
|
}
|
|
227
|
-
/**
|
|
224
|
+
/** Modal, opened by an `open-modal` event carrying its id. */
|
|
228
225
|
export function modal(props, slot = '') {
|
|
229
226
|
const { id, alignment = 'start', width = 'sm', closeButton = true, closeByClickingAway = true, closeByEscaping = true } = props;
|
|
230
227
|
const hasHeading = !!props.heading, hasDescription = !!props.description && hasHeading;
|
|
@@ -233,7 +230,7 @@ export function modal(props, slot = '') {
|
|
|
233
230
|
const key = (part) => `${props.livewireId ? `${props.livewireId}.` : ''}modal.${id}.${part}`;
|
|
234
231
|
const close = `$dispatch('close-modal', { id: ${js(id)} })`;
|
|
235
232
|
const iconColor = props.iconColor ?? 'primary';
|
|
236
|
-
//
|
|
233
|
+
// Scrolling window (slide-over, sticky header or footer): the focus trap goes on the window itself, not its container.
|
|
237
234
|
const trapOnWindow = props.slideOver === true || props.stickyHeader === true || props.stickyFooter === true;
|
|
238
235
|
const closeHtml = closeButton ? iconButton({ color: 'gray', icon: 'o-x-mark', label: t('filament::components/modal.actions.close.label'), attributes: { tabindex: closeByEscaping ? '-1' : null, 'x-on:click': close, class: 'fi-modal-close-btn' } }).replace('fi-icon fi-size-md', 'fi-icon fi-size-lg') : '';
|
|
239
236
|
const iconHtml = props.icon ? `<div class="fi-modal-icon-ctn"><div class="${cls([...colorMark(iconColor), 'fi-modal-icon-bg'])}">${icon(props.icon, { size: 'lg' })}</div></div>` : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/support",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "Shared utilities, colors, icons, views and translations for Navette.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"navette",
|
|
45
|
-
"filament",
|
|
46
45
|
"admin",
|
|
47
46
|
"panel",
|
|
48
47
|
"livewire",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# Badge
|
|
1
|
+
<%# Badge without icon or delete button. %>
|
|
2
2
|
<span class="<%- cls([...colorClasses, 'fi-badge', 'fi-size-' + (locals.size ?? 'md')]) %>">
|
|
3
3
|
<span class="fi-badge-label-ctn">
|
|
4
4
|
<span class="fi-badge-label">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
`extra
|
|
1
|
+
<%# Gray icon button without badge.
|
|
2
|
+
`extra`: attributes placed after the loading attributes; with `after` (modal close button), `extra` goes before `type` and `after` comes last. %>
|
|
3
3
|
<% const lead = locals.after ? (locals.extra ? extra.toHtml() + ' ' : '') : ''; const tail = locals.after ? after.toHtml() : (locals.extra ? extra.toHtml() : ''); %>
|
|
4
4
|
<button class="fi-icon-btn fi-size-<%- locals.size ?? 'md' %> <%- className %>" title="<%= label %>" aria-label="<%= label %>" <%- lead %>type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled"<% if (tail) { %> <%- tail %><% } %>>
|
|
5
5
|
<%- icon(iconName, { size: locals.iconSize ?? 'md' }) %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<%# Pagination
|
|
2
|
-
|
|
1
|
+
<%# Pagination. Options: simple (previous / next only), extremeLinks (first / last), property (property bound to the per-page count).
|
|
2
|
+
Cursor pagination is not supported. %>
|
|
3
3
|
<% const simple = locals.simple === true, extremeLinks = locals.extremeLinks === true, property = locals.property ?? 'tableRecordsPerPage'; %>
|
|
4
4
|
<% const perPageSelect = () => { %>
|
|
5
5
|
<select class="fi-select-input" wire:model.live="<%= property %>">
|