@vertebr/react 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{VertebrAdmin.d.ts → components/admin/VertebrAdmin.d.ts} +3 -1
- package/dist/components/admin/VertebrAdmin.d.ts.map +1 -0
- package/dist/components/admin/VertebrAdmin.js +375 -0
- package/dist/components/config/ConfigForm.d.ts +3 -0
- package/dist/components/config/ConfigForm.d.ts.map +1 -0
- package/dist/components/config/ConfigForm.js +1 -0
- package/dist/components/config/DescriptionInput.d.ts +18 -0
- package/dist/components/config/DescriptionInput.d.ts.map +1 -0
- package/dist/components/config/DescriptionInput.js +152 -0
- package/dist/components/config/ImagePickerModal.d.ts +18 -0
- package/dist/components/config/ImagePickerModal.d.ts.map +1 -0
- package/dist/components/config/ImagePickerModal.js +120 -0
- package/dist/components/config/PriceInput.d.ts +12 -0
- package/dist/components/config/PriceInput.d.ts.map +1 -0
- package/dist/components/config/PriceInput.js +20 -0
- package/dist/components/config/form/ConfigField.d.ts +21 -0
- package/dist/components/config/form/ConfigField.d.ts.map +1 -0
- package/dist/components/config/form/ConfigField.js +48 -0
- package/dist/components/config/form/ConfigForm.d.ts +4 -0
- package/dist/components/config/form/ConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/ConfigForm.js +70 -0
- package/dist/components/config/form/ConfigFormActions.d.ts +10 -0
- package/dist/components/config/form/ConfigFormActions.d.ts.map +1 -0
- package/dist/components/config/form/ConfigFormActions.js +4 -0
- package/dist/components/config/form/RecordConfigForm.d.ts +21 -0
- package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/RecordConfigForm.js +37 -0
- package/dist/components/config/form/SettingsConfigForm.d.ts +21 -0
- package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -0
- package/dist/components/config/form/SettingsConfigForm.js +7 -0
- package/dist/components/config/form/helpers.d.ts +16 -0
- package/dist/components/config/form/helpers.d.ts.map +1 -0
- package/dist/components/config/form/helpers.js +186 -0
- package/dist/components/config/form/types.d.ts +27 -0
- package/dist/components/config/form/types.d.ts.map +1 -0
- package/dist/{EnvSelect.d.ts → components/env/EnvSelect.d.ts} +1 -1
- package/dist/components/env/EnvSelect.d.ts.map +1 -0
- package/dist/components/gallery/GalleryBoard.d.ts +14 -0
- package/dist/components/gallery/GalleryBoard.d.ts.map +1 -0
- package/dist/components/gallery/GalleryBoard.js +14 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +6 -0
- package/dist/{MetricsBoard.d.ts → components/metrics/MetricsBoard.d.ts} +1 -1
- package/dist/components/metrics/MetricsBoard.d.ts.map +1 -0
- package/dist/components/resources/BulkImageUploadModal.d.ts +17 -0
- package/dist/components/resources/BulkImageUploadModal.d.ts.map +1 -0
- package/dist/components/resources/BulkImageUploadModal.js +47 -0
- package/dist/components/resources/ColumnVisibilityMenu.d.ts +11 -0
- package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -0
- package/dist/components/resources/ColumnVisibilityMenu.js +40 -0
- package/dist/components/resources/ImageUploadControl.d.ts +10 -0
- package/dist/components/resources/ImageUploadControl.d.ts.map +1 -0
- package/dist/components/resources/ImageUploadControl.js +50 -0
- package/dist/components/resources/ResourceCell.d.ts +10 -0
- package/dist/components/resources/ResourceCell.d.ts.map +1 -0
- package/dist/components/resources/ResourceCell.js +100 -0
- package/dist/components/resources/ResourceTable.d.ts +34 -0
- package/dist/components/resources/ResourceTable.d.ts.map +1 -0
- package/dist/components/resources/ResourceTable.js +249 -0
- package/dist/components/resources/ResourceViewModal.d.ts +11 -0
- package/dist/components/resources/ResourceViewModal.d.ts.map +1 -0
- package/dist/components/resources/ResourceViewModal.js +44 -0
- package/dist/components/theme/ThemeToggle.d.ts +7 -0
- package/dist/components/theme/ThemeToggle.d.ts.map +1 -0
- package/dist/components/theme/ThemeToggle.js +10 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/{useVertebrControls.d.ts → hooks/useVertebrControls.d.ts} +2 -2
- package/dist/hooks/useVertebrControls.d.ts.map +1 -0
- package/dist/{useVertebrControls.js → hooks/useVertebrControls.js} +11 -21
- package/dist/hooks/useVertebrTheme.d.ts +7 -0
- package/dist/hooks/useVertebrTheme.d.ts.map +1 -0
- package/dist/hooks/useVertebrTheme.js +38 -0
- package/dist/index.d.ts +20 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/lib/description.d.ts +18 -0
- package/dist/lib/description.d.ts.map +1 -0
- package/dist/lib/description.js +85 -0
- package/dist/lib/images.d.ts +14 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/lib/images.js +63 -0
- package/dist/lib/price.d.ts +15 -0
- package/dist/lib/price.d.ts.map +1 -0
- package/dist/lib/price.js +50 -0
- package/dist/styles/auth.css +39 -0
- package/dist/styles/base.css +70 -0
- package/dist/styles/buttons.css +86 -0
- package/dist/styles/cells.css +178 -0
- package/dist/styles/columns.css +103 -0
- package/dist/styles/form.css +773 -0
- package/dist/styles/gallery.css +90 -0
- package/dist/styles/index.css +18 -0
- package/dist/styles/main.css +89 -0
- package/dist/styles/modal.css +225 -0
- package/dist/styles/nav.css +92 -0
- package/dist/styles/panel.css +215 -0
- package/dist/styles/select.css +151 -0
- package/dist/styles/sidebar.css +42 -0
- package/dist/styles/states.css +169 -0
- package/dist/styles/table.css +101 -0
- package/dist/styles/theme-toggle.css +87 -0
- package/dist/styles.css +2 -997
- package/dist/themes/dark.css +55 -0
- package/dist/themes/index.css +2 -0
- package/dist/themes/light.css +102 -0
- package/package.json +13 -3
- package/dist/ConfigForm.d.ts +0 -9
- package/dist/ConfigForm.d.ts.map +0 -1
- package/dist/ConfigForm.js +0 -87
- package/dist/EnvSelect.d.ts.map +0 -1
- package/dist/MetricsBoard.d.ts.map +0 -1
- package/dist/ResourceTable.d.ts +0 -23
- package/dist/ResourceTable.d.ts.map +0 -1
- package/dist/ResourceTable.js +0 -112
- package/dist/VertebrAdmin.d.ts.map +0 -1
- package/dist/VertebrAdmin.js +0 -269
- package/dist/client.d.ts +0 -32
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -65
- package/dist/types.d.ts +0 -103
- package/dist/types.d.ts.map +0 -1
- package/dist/useVertebrControls.d.ts.map +0 -1
- /package/dist/{types.js → components/config/form/types.js} +0 -0
- /package/dist/{EnvSelect.js → components/env/EnvSelect.js} +0 -0
- /package/dist/{MetricsBoard.js → components/metrics/MetricsBoard.js} +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const DESCRIPTION_KEY = /(description|body|content|summary|bio|copy|caption|details)/i;
|
|
2
|
+
export function isDescriptionField(key, field) {
|
|
3
|
+
if (field?.type === "richtext")
|
|
4
|
+
return true;
|
|
5
|
+
const hay = `${key} ${field?.label ?? ""}`;
|
|
6
|
+
return DESCRIPTION_KEY.test(hay);
|
|
7
|
+
}
|
|
8
|
+
export function descriptionStats(value) {
|
|
9
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
10
|
+
if (!text)
|
|
11
|
+
return { characters: 0, words: 0 };
|
|
12
|
+
return {
|
|
13
|
+
characters: text.length,
|
|
14
|
+
words: text.split(/\s+/).filter(Boolean).length,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function wrap(text, start, end, before, after, placeholder) {
|
|
18
|
+
const selected = text.slice(start, end) || placeholder;
|
|
19
|
+
const inserted = before + selected + after;
|
|
20
|
+
return {
|
|
21
|
+
next: text.slice(0, start) + inserted + text.slice(end),
|
|
22
|
+
inserted,
|
|
23
|
+
replaceStart: start,
|
|
24
|
+
replaceEnd: end,
|
|
25
|
+
selectionStart: start + before.length,
|
|
26
|
+
selectionEnd: start + before.length + selected.length,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function prefixLines(text, start, end, prefixFor) {
|
|
30
|
+
const lineStart = text.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
|
|
31
|
+
const lineEndIndex = text.indexOf("\n", end);
|
|
32
|
+
const lineEnd = lineEndIndex === -1 ? text.length : lineEndIndex;
|
|
33
|
+
const block = text.slice(lineStart, lineEnd) || "";
|
|
34
|
+
const lines = block.split("\n");
|
|
35
|
+
const inserted = lines
|
|
36
|
+
.map((line, index) => prefixFor(index, line))
|
|
37
|
+
.join("\n");
|
|
38
|
+
return {
|
|
39
|
+
next: text.slice(0, lineStart) + inserted + text.slice(lineEnd),
|
|
40
|
+
inserted,
|
|
41
|
+
replaceStart: lineStart,
|
|
42
|
+
replaceEnd: lineEnd,
|
|
43
|
+
selectionStart: lineStart,
|
|
44
|
+
selectionEnd: lineStart + inserted.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Apply a markdown format command around the current selection. */
|
|
48
|
+
export function applyMarkdown(text, start, end, command) {
|
|
49
|
+
const from = Math.max(0, Math.min(start, end, text.length));
|
|
50
|
+
const to = Math.max(0, Math.min(Math.max(start, end), text.length));
|
|
51
|
+
switch (command) {
|
|
52
|
+
case "bold":
|
|
53
|
+
return wrap(text, from, to, "**", "**", "bold text");
|
|
54
|
+
case "italic":
|
|
55
|
+
return wrap(text, from, to, "*", "*", "italic text");
|
|
56
|
+
case "link": {
|
|
57
|
+
const selected = text.slice(from, to) || "link text";
|
|
58
|
+
const inserted = `[${selected}](https://)`;
|
|
59
|
+
const urlStart = from + selected.length + 3;
|
|
60
|
+
return {
|
|
61
|
+
next: text.slice(0, from) + inserted + text.slice(to),
|
|
62
|
+
inserted,
|
|
63
|
+
replaceStart: from,
|
|
64
|
+
replaceEnd: to,
|
|
65
|
+
selectionStart: urlStart,
|
|
66
|
+
selectionEnd: urlStart + "https://".length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case "heading":
|
|
70
|
+
return prefixLines(text, from, to, (_index, line) => {
|
|
71
|
+
const stripped = line.replace(/^#{1,6}\s+/, "");
|
|
72
|
+
return `## ${stripped || "Heading"}`;
|
|
73
|
+
});
|
|
74
|
+
case "ul":
|
|
75
|
+
return prefixLines(text, from, to, (_index, line) => {
|
|
76
|
+
const stripped = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
77
|
+
return `- ${stripped || "List item"}`;
|
|
78
|
+
});
|
|
79
|
+
case "ol":
|
|
80
|
+
return prefixLines(text, from, to, (index, line) => {
|
|
81
|
+
const stripped = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
82
|
+
return `${index + 1}. ${stripped || "List item"}`;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FieldSchema } from "@vertebr/client";
|
|
2
|
+
export type RecordImage = {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
field: string;
|
|
7
|
+
};
|
|
8
|
+
declare function looksLikeUrl(value: string): boolean;
|
|
9
|
+
export declare function isImageField(key: string, field?: Pick<FieldSchema, "label" | "type">): boolean;
|
|
10
|
+
/** Flatten record image fields (single URL or URL lists) for the editor picker. */
|
|
11
|
+
export declare function collectRecordImages(schema: Record<string, FieldSchema>, values: Record<string, unknown> | undefined): RecordImage[];
|
|
12
|
+
export declare function urlsFromValue(value: unknown): string[];
|
|
13
|
+
export { looksLikeUrl };
|
|
14
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/lib/images.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,iBAAS,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,GAC1C,OAAO,CAIT;AAqBD,mFAAmF;AACnF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1C,WAAW,EAAE,CAmBf;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAYtD;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function looksLikeUrl(value) {
|
|
2
|
+
return /^https?:\/\//i.test(value) || value.startsWith("//");
|
|
3
|
+
}
|
|
4
|
+
export function isImageField(key, field) {
|
|
5
|
+
if (field?.type === "file[]")
|
|
6
|
+
return true;
|
|
7
|
+
const hay = `${key} ${field?.label ?? ""}`;
|
|
8
|
+
return /(image|img|photo|avatar|thumbnail|thumb|cover)/i.test(hay);
|
|
9
|
+
}
|
|
10
|
+
function urlFromUnknown(value) {
|
|
11
|
+
if (typeof value === "string" && looksLikeUrl(value)) {
|
|
12
|
+
return { url: value };
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === "object" && "url" in value) {
|
|
15
|
+
const url = value.url;
|
|
16
|
+
const name = value.name;
|
|
17
|
+
if (typeof url === "string" && looksLikeUrl(url)) {
|
|
18
|
+
return {
|
|
19
|
+
url,
|
|
20
|
+
name: typeof name === "string" ? name : undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Flatten record image fields (single URL or URL lists) for the editor picker. */
|
|
27
|
+
export function collectRecordImages(schema, values) {
|
|
28
|
+
const images = [];
|
|
29
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
30
|
+
if (!isImageField(key, field))
|
|
31
|
+
continue;
|
|
32
|
+
const label = field.label?.trim() || key;
|
|
33
|
+
const value = values?.[key];
|
|
34
|
+
const items = Array.isArray(value) ? value : [value];
|
|
35
|
+
items.forEach((item, index) => {
|
|
36
|
+
const parsed = urlFromUnknown(item);
|
|
37
|
+
if (!parsed)
|
|
38
|
+
return;
|
|
39
|
+
images.push({
|
|
40
|
+
id: `${key}:${index}`,
|
|
41
|
+
url: parsed.url,
|
|
42
|
+
name: parsed.name || (items.length > 1 ? `${label} ${index + 1}` : label),
|
|
43
|
+
field: key,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return images;
|
|
48
|
+
}
|
|
49
|
+
export function urlsFromValue(value) {
|
|
50
|
+
const items = Array.isArray(value)
|
|
51
|
+
? value
|
|
52
|
+
: value == null || value === ""
|
|
53
|
+
? []
|
|
54
|
+
: [value];
|
|
55
|
+
const urls = [];
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const parsed = urlFromUnknown(item);
|
|
58
|
+
if (parsed)
|
|
59
|
+
urls.push(parsed.url);
|
|
60
|
+
}
|
|
61
|
+
return urls;
|
|
62
|
+
}
|
|
63
|
+
export { looksLikeUrl };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FieldSchema } from "@vertebr/client";
|
|
2
|
+
export declare function isCentsField(key: string, field?: Pick<FieldSchema, "label">): boolean;
|
|
3
|
+
export declare function isPriceField(key: string, field?: Pick<FieldSchema, "label">): boolean;
|
|
4
|
+
export declare function dollarsFromStored(value: number, asCents: boolean): number;
|
|
5
|
+
export declare function storedFromDollars(dollars: number, asCents: boolean): number;
|
|
6
|
+
/** Format a stored number for display (`3800` cents → `$38.00`). */
|
|
7
|
+
export declare function formatPrice(value: number, options?: {
|
|
8
|
+
asCents?: boolean;
|
|
9
|
+
currency?: string;
|
|
10
|
+
}): string;
|
|
11
|
+
/** Value shown in an editable money input (`3800` → `38.00`). */
|
|
12
|
+
export declare function formatPriceInput(value: unknown, asCents?: boolean): string;
|
|
13
|
+
/** Parse typed money (`$38`, `38.00`, `38`) back to stored units. */
|
|
14
|
+
export declare function parsePriceInput(raw: string, asCents?: boolean): number | undefined;
|
|
15
|
+
//# sourceMappingURL=price.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price.d.ts","sourceRoot":"","sources":["../../src/lib/price.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,GACjC,OAAO,CAET;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,GACjC,OAAO,CAGT;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAEzE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAE3E;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,MAAM,CAcR;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,OAAO,UAAO,GACb,MAAM,CAGR;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,OAAO,UAAO,GACb,MAAM,GAAG,SAAS,CAQpB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const CENTS_KEY = /(cents|priceCents|amountCents)/i;
|
|
2
|
+
const PRICE_KEY = /(price|amount|cost|total|msrp)/i;
|
|
3
|
+
export function isCentsField(key, field) {
|
|
4
|
+
return CENTS_KEY.test(key) || /cents/i.test(field?.label ?? "");
|
|
5
|
+
}
|
|
6
|
+
export function isPriceField(key, field) {
|
|
7
|
+
if (isCentsField(key, field))
|
|
8
|
+
return true;
|
|
9
|
+
return PRICE_KEY.test(key) || PRICE_KEY.test(field?.label ?? "");
|
|
10
|
+
}
|
|
11
|
+
export function dollarsFromStored(value, asCents) {
|
|
12
|
+
return asCents ? value / 100 : value;
|
|
13
|
+
}
|
|
14
|
+
export function storedFromDollars(dollars, asCents) {
|
|
15
|
+
return asCents ? Math.round(dollars * 100) : dollars;
|
|
16
|
+
}
|
|
17
|
+
/** Format a stored number for display (`3800` cents → `$38.00`). */
|
|
18
|
+
export function formatPrice(value, options) {
|
|
19
|
+
const asCents = options?.asCents ?? true;
|
|
20
|
+
const currency = options?.currency ?? "USD";
|
|
21
|
+
const amount = dollarsFromStored(value, asCents);
|
|
22
|
+
try {
|
|
23
|
+
return new Intl.NumberFormat(undefined, {
|
|
24
|
+
style: "currency",
|
|
25
|
+
currency,
|
|
26
|
+
minimumFractionDigits: 2,
|
|
27
|
+
maximumFractionDigits: 2,
|
|
28
|
+
}).format(amount);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return `$${amount.toFixed(2)}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Value shown in an editable money input (`3800` → `38.00`). */
|
|
35
|
+
export function formatPriceInput(value, asCents = true) {
|
|
36
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
37
|
+
return "";
|
|
38
|
+
return dollarsFromStored(value, asCents).toFixed(2);
|
|
39
|
+
}
|
|
40
|
+
/** Parse typed money (`$38`, `38.00`, `38`) back to stored units. */
|
|
41
|
+
export function parsePriceInput(raw, asCents = true) {
|
|
42
|
+
const cleaned = raw.replace(/[^0-9.-]/g, "");
|
|
43
|
+
if (!cleaned || cleaned === "-" || cleaned === "." || cleaned === "-.") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const dollars = Number(cleaned);
|
|
47
|
+
if (!Number.isFinite(dollars))
|
|
48
|
+
return undefined;
|
|
49
|
+
return storedFromDollars(dollars, asCents);
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* ── Auth ────────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
.vertebr-auth {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 0.5rem;
|
|
7
|
+
padding: 0.15rem 0.2rem 0.35rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.vertebr-auth-hint {
|
|
11
|
+
margin: 0;
|
|
12
|
+
color: var(--vb-text-muted);
|
|
13
|
+
font-size: 0.78rem;
|
|
14
|
+
line-height: 1.4;
|
|
15
|
+
word-break: break-all;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.vertebr-auth-input {
|
|
19
|
+
width: 100%;
|
|
20
|
+
border: 1px solid var(--vb-border);
|
|
21
|
+
background: var(--vb-bg);
|
|
22
|
+
color: var(--vb-text);
|
|
23
|
+
border-radius: var(--vb-radius-sm);
|
|
24
|
+
padding: 0.55rem 0.65rem;
|
|
25
|
+
font: inherit;
|
|
26
|
+
font-size: 0.85rem;
|
|
27
|
+
transition: border-color 140ms ease, box-shadow 140ms ease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.vertebr-auth-input:focus {
|
|
31
|
+
outline: none;
|
|
32
|
+
border-color: var(--vb-accent-dim);
|
|
33
|
+
box-shadow: 0 0 0 3px var(--vb-accent-soft);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.vertebr-auth-actions {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: 0.5rem;
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
html:has(.vertebr-root),
|
|
2
|
+
html:has(.vertebr-login),
|
|
3
|
+
html:has(.vertebr-loading),
|
|
4
|
+
html:has(.vertebr-error) {
|
|
5
|
+
background: var(--vb-bg);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
html:has(.vertebr-root) body,
|
|
9
|
+
html:has(.vertebr-login) body,
|
|
10
|
+
html:has(.vertebr-loading) body,
|
|
11
|
+
html:has(.vertebr-error) body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
background: var(--vb-bg);
|
|
16
|
+
color: var(--vb-text);
|
|
17
|
+
font-family: var(--vb-font);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.vertebr-root {
|
|
21
|
+
position: relative;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
max-height: 100vh;
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
width: 100%;
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-columns: var(--vb-sidebar-width) 1fr;
|
|
30
|
+
background:
|
|
31
|
+
radial-gradient(
|
|
32
|
+
900px 480px at 0% -10%,
|
|
33
|
+
var(--vb-glow-accent),
|
|
34
|
+
transparent 55%
|
|
35
|
+
),
|
|
36
|
+
radial-gradient(
|
|
37
|
+
700px 400px at 100% 100%,
|
|
38
|
+
var(--vb-glow-cool),
|
|
39
|
+
transparent 50%
|
|
40
|
+
),
|
|
41
|
+
linear-gradient(180deg, var(--vb-bg-deep) 0%, var(--vb-bg) 45%);
|
|
42
|
+
color: var(--vb-text);
|
|
43
|
+
font-family: var(--vb-font);
|
|
44
|
+
font-size: 15px;
|
|
45
|
+
line-height: 1.45;
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
isolation: isolate;
|
|
48
|
+
overflow: clip;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.vertebr-root *,
|
|
52
|
+
.vertebr-root *::before,
|
|
53
|
+
.vertebr-root *::after {
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Guard against host-page CSS leaking into the sidebar. */
|
|
58
|
+
.vertebr-sidebar,
|
|
59
|
+
.vertebr-sidebar-top,
|
|
60
|
+
.vertebr-sidebar-bottom,
|
|
61
|
+
.vertebr-nav,
|
|
62
|
+
.vertebr-nav-group,
|
|
63
|
+
.vertebr-brand,
|
|
64
|
+
.vertebr-env,
|
|
65
|
+
.vertebr-auth,
|
|
66
|
+
.vertebr-theme {
|
|
67
|
+
position: relative;
|
|
68
|
+
float: none;
|
|
69
|
+
inset: auto;
|
|
70
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* ── Buttons / status ────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
.vertebr-btn {
|
|
4
|
+
appearance: none;
|
|
5
|
+
border: 1px solid var(--vb-border);
|
|
6
|
+
background: var(--vb-bg-muted);
|
|
7
|
+
color: var(--vb-text);
|
|
8
|
+
border-radius: var(--vb-radius-sm);
|
|
9
|
+
padding: 0.52rem 0.95rem;
|
|
10
|
+
font: inherit;
|
|
11
|
+
font-size: 0.88rem;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
transition:
|
|
15
|
+
background 120ms ease,
|
|
16
|
+
border-color 120ms ease,
|
|
17
|
+
transform 80ms ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.vertebr-btn:hover:not(:disabled) {
|
|
21
|
+
background: var(--vb-bg-hover);
|
|
22
|
+
border-color: var(--vb-border-strong);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.vertebr-btn:active:not(:disabled) {
|
|
26
|
+
transform: translateY(1px);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.vertebr-btn-primary {
|
|
30
|
+
background: linear-gradient(
|
|
31
|
+
180deg,
|
|
32
|
+
var(--vb-btn-primary-from) 0%,
|
|
33
|
+
var(--vb-btn-primary-to) 100%
|
|
34
|
+
);
|
|
35
|
+
border-color: var(--vb-btn-primary-border);
|
|
36
|
+
color: var(--vb-accent-contrast);
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
box-shadow: 0 1px 0 var(--vb-surface-tint-strong) inset;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.vertebr-btn-primary:hover:not(:disabled) {
|
|
42
|
+
background: linear-gradient(
|
|
43
|
+
180deg,
|
|
44
|
+
var(--vb-btn-primary-hover-from) 0%,
|
|
45
|
+
var(--vb-btn-primary-hover-to) 100%
|
|
46
|
+
);
|
|
47
|
+
border-color: var(--vb-btn-primary-hover-border);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.vertebr-btn:disabled {
|
|
51
|
+
opacity: 0.5;
|
|
52
|
+
cursor: not-allowed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.vertebr-btn-quiet {
|
|
56
|
+
border-color: transparent;
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: var(--vb-text-muted);
|
|
59
|
+
box-shadow: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.vertebr-btn-quiet:hover:not(:disabled) {
|
|
63
|
+
background: var(--vb-bg-hover);
|
|
64
|
+
border-color: transparent;
|
|
65
|
+
color: var(--vb-text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.vertebr-btn-danger-quiet:hover:not(:disabled) {
|
|
69
|
+
background: var(--vb-danger-soft);
|
|
70
|
+
color: var(--vb-danger);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.vertebr-status {
|
|
74
|
+
font-family: var(--vb-mono);
|
|
75
|
+
font-size: 0.75rem;
|
|
76
|
+
color: var(--vb-text-muted);
|
|
77
|
+
letter-spacing: 0.02em;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.vertebr-status[data-tone="ok"] {
|
|
81
|
+
color: var(--vb-ok);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.vertebr-status[data-tone="danger"] {
|
|
85
|
+
color: var(--vb-danger);
|
|
86
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* ── Cells ───────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
.vertebr-table tr:last-child td {
|
|
4
|
+
border-bottom: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.vertebr-table td:has(.vertebr-cell-clamp),
|
|
8
|
+
.vertebr-table td:has(.vertebr-cell-media),
|
|
9
|
+
.vertebr-table td:has(.vertebr-cell-link) {
|
|
10
|
+
white-space: normal;
|
|
11
|
+
min-width: 10rem;
|
|
12
|
+
max-width: 16rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.vertebr-cell-empty {
|
|
16
|
+
color: var(--vb-text-subtle);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.vertebr-cell-text,
|
|
20
|
+
.vertebr-cell-number,
|
|
21
|
+
.vertebr-cell-money,
|
|
22
|
+
.vertebr-cell-datetime {
|
|
23
|
+
color: var(--vb-text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.vertebr-cell-number,
|
|
27
|
+
.vertebr-cell-money {
|
|
28
|
+
font-variant-numeric: tabular-nums;
|
|
29
|
+
font-family: var(--vb-mono);
|
|
30
|
+
font-size: 0.86rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.vertebr-cell-money {
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.vertebr-cell-datetime {
|
|
38
|
+
font-family: var(--vb-mono);
|
|
39
|
+
font-size: 0.8rem;
|
|
40
|
+
color: var(--vb-text-muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.vertebr-cell-clamp {
|
|
44
|
+
display: -webkit-box;
|
|
45
|
+
-webkit-line-clamp: 2;
|
|
46
|
+
-webkit-box-orient: vertical;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
white-space: normal;
|
|
49
|
+
line-height: 1.35;
|
|
50
|
+
color: var(--vb-text-muted);
|
|
51
|
+
max-width: 16rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.vertebr-cell-link {
|
|
55
|
+
display: inline-block;
|
|
56
|
+
max-width: 14rem;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
text-overflow: ellipsis;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
color: var(--vb-accent);
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
font-size: 0.82rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.vertebr-cell-link:hover {
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.vertebr-cell-media {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 0.65rem;
|
|
73
|
+
max-width: 16rem;
|
|
74
|
+
text-decoration: none;
|
|
75
|
+
color: inherit;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.vertebr-cell-media img {
|
|
79
|
+
width: 44px;
|
|
80
|
+
height: 44px;
|
|
81
|
+
object-fit: cover;
|
|
82
|
+
border-radius: var(--vb-radius-sm);
|
|
83
|
+
border: 1px solid var(--vb-border);
|
|
84
|
+
background: var(--vb-bg-muted);
|
|
85
|
+
flex: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.vertebr-cell-truncate {
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
text-overflow: ellipsis;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
font-size: 0.75rem;
|
|
93
|
+
color: var(--vb-text-subtle);
|
|
94
|
+
font-family: var(--vb-mono);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.vertebr-cell-json {
|
|
98
|
+
display: inline-block;
|
|
99
|
+
max-width: 14rem;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
text-overflow: ellipsis;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
font-family: var(--vb-mono);
|
|
104
|
+
font-size: 0.75rem;
|
|
105
|
+
color: var(--vb-text-muted);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.vertebr-badge {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
padding: 0.18rem 0.55rem;
|
|
113
|
+
font-size: 0.72rem;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
letter-spacing: 0.04em;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
border: 1px solid var(--vb-border);
|
|
118
|
+
color: var(--vb-text-muted);
|
|
119
|
+
background: var(--vb-surface-tint);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.vertebr-badge[data-tone="ok"] {
|
|
123
|
+
color: var(--vb-ok);
|
|
124
|
+
border-color: color-mix(in srgb, var(--vb-ok) 40%, transparent);
|
|
125
|
+
background: var(--vb-ok-soft);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.vertebr-badge[data-tone="neutral"] {
|
|
129
|
+
color: var(--vb-text-subtle);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.vertebr-chip-row {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-wrap: wrap;
|
|
135
|
+
gap: 0.3rem;
|
|
136
|
+
max-width: 14rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.vertebr-chip {
|
|
140
|
+
border: 1px solid var(--vb-border);
|
|
141
|
+
border-radius: 999px;
|
|
142
|
+
padding: 0.1rem 0.45rem;
|
|
143
|
+
font-size: 0.72rem;
|
|
144
|
+
color: var(--vb-text-muted);
|
|
145
|
+
background: var(--vb-surface-tint);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.vertebr-empty {
|
|
149
|
+
flex: 1 1 auto;
|
|
150
|
+
display: grid;
|
|
151
|
+
place-items: center;
|
|
152
|
+
padding: 3rem 1.5rem;
|
|
153
|
+
text-align: center;
|
|
154
|
+
color: var(--vb-text-muted);
|
|
155
|
+
font-size: 0.92rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.vertebr-cell-media-detail {
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
align-items: flex-start;
|
|
161
|
+
max-width: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.vertebr-cell-media-detail img {
|
|
165
|
+
width: min(100%, 280px);
|
|
166
|
+
height: auto;
|
|
167
|
+
max-height: 280px;
|
|
168
|
+
object-fit: cover;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.vertebr-cell-prose {
|
|
172
|
+
margin: 0;
|
|
173
|
+
white-space: pre-wrap;
|
|
174
|
+
line-height: 1.5;
|
|
175
|
+
color: var(--vb-text);
|
|
176
|
+
max-width: 40rem;
|
|
177
|
+
}
|
|
178
|
+
|