@valbuild/next 0.100.0 → 0.101.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/README.md
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
- [Image](#image)
|
|
56
56
|
- [keyOf](#keyof)
|
|
57
57
|
- [Route](#route)
|
|
58
|
+
- [Color](#color)
|
|
59
|
+
- [Date](#date)
|
|
60
|
+
- [DateTime](#datetime)
|
|
58
61
|
|
|
59
62
|
## Installation
|
|
60
63
|
|
|
@@ -886,6 +889,235 @@ export default c.define("/components/link.val.ts", linkSchema, {
|
|
|
886
889
|
});
|
|
887
890
|
```
|
|
888
891
|
|
|
892
|
+
## Color
|
|
893
|
+
|
|
894
|
+
The `color` schema represents a color, stored as a CSS color string. Because the value is a plain CSS string, it can be used directly in a `style` attribute or set as a CSS custom property - there is nothing to convert in your components.
|
|
895
|
+
|
|
896
|
+
### Color Schema
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
s.color(); // <- Schema<string>
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Output format
|
|
903
|
+
|
|
904
|
+
The `format` option decides which CSS notation the color is stored in. It defaults to `"hsl"`.
|
|
905
|
+
|
|
906
|
+
```ts
|
|
907
|
+
s.color(); // hsl(217.22 91.22% 59.8%)
|
|
908
|
+
s.color({ format: "hex" }); // #3b82f6
|
|
909
|
+
s.color({ format: "rgb" }); // rgb(59 130 246)
|
|
910
|
+
s.color({ format: "oklch" }); // oklch(0.6231 0.188 259.81)
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
The Val editor writes the value back in this format, so an editor who pastes `#3b82f6` into a field declared as `s.color()` gets `hsl(217.22 91.22% 59.8%)` stored.
|
|
914
|
+
|
|
915
|
+
### Transparency
|
|
916
|
+
|
|
917
|
+
Colors are fully opaque unless you opt into an alpha channel with `alpha: true`. A color with an alpha channel is a validation error in a field that does not allow it, so you can be sure an opaque color stays opaque.
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
s.color({ format: "hsl", alpha: true }); // hsl(217.22 91.22% 59.8% / 0.5)
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
With `alpha: true` the editor also gets an alpha slider next to the color picker.
|
|
924
|
+
|
|
925
|
+
### Validation
|
|
926
|
+
|
|
927
|
+
Validation is lenient about the syntax and strict about the format:
|
|
928
|
+
|
|
929
|
+
- both the modern and the legacy notation of the declared format are accepted, so `hsl(0 100% 50%)` and `hsl(0, 100%, 50%)` are both valid `hsl` values, and `#f00` is a valid `hex` value
|
|
930
|
+
- a color written in another format is an error which tells you the equivalent value in the right notation:
|
|
931
|
+
`Expected a color in the 'hsl' format (e.g. 'hsl(217.22 91.22% 59.8%)'), got '#ff0000'. Did you mean 'hsl(0 100% 50%)'?`
|
|
932
|
+
- named colors (`red`), `lab()`, `color()` and `color-mix()` are not supported
|
|
933
|
+
|
|
934
|
+
Colors can be nullable and can use `.describe()` and `.validate()` like any other schema:
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
s.color().nullable().describe("Optional highlight color");
|
|
938
|
+
|
|
939
|
+
s.color({ format: "hex" }).validate((color) => {
|
|
940
|
+
if (color === "#000000") {
|
|
941
|
+
return "Pure black is too harsh - pick a dark grey instead";
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
});
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Initializing color content
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
import { s, c, type t } from "../val.config";
|
|
951
|
+
|
|
952
|
+
export const schema = s.object({
|
|
953
|
+
brand: s.color().describe("Primary brand color"),
|
|
954
|
+
background: s.color({ format: "hex" }).describe("Page background"),
|
|
955
|
+
overlay: s.color({ format: "hsl", alpha: true }).describe("Overlay tint"),
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
export type Theme = t.inferSchema<typeof schema>;
|
|
959
|
+
export default c.define("/content/theme.val.ts", schema, {
|
|
960
|
+
brand: "hsl(217.22 91.22% 59.8%)",
|
|
961
|
+
background: "#0b1020",
|
|
962
|
+
overlay: "hsl(217.22 91.22% 59.8% / 0.15)",
|
|
963
|
+
});
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Using colors
|
|
967
|
+
|
|
968
|
+
The value is a string, so use it wherever CSS expects a color:
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
import { fetchVal } from "../val/rsc";
|
|
972
|
+
import themeVal from "../content/theme.val";
|
|
973
|
+
|
|
974
|
+
export default async function Hero() {
|
|
975
|
+
const theme = await fetchVal(themeVal);
|
|
976
|
+
return (
|
|
977
|
+
<section style={{ background: theme.background, color: theme.brand }}>
|
|
978
|
+
<h1 style={{ borderBottom: `2px solid ${theme.brand}` }}>Hello</h1>
|
|
979
|
+
</section>
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
To hand a color to a stylesheet instead, set it as a CSS custom property:
|
|
985
|
+
|
|
986
|
+
```tsx
|
|
987
|
+
<div style={{ "--brand": theme.brand } as React.CSSProperties}>
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**NOTE**: colors are not steganographically tagged, since the value ends up in CSS where the invisible characters would break the declaration. Colors therefore do not participate in click-then-edit visual editing (the same is true of dates) - edit them from the studio instead.
|
|
991
|
+
|
|
992
|
+
## Date
|
|
993
|
+
|
|
994
|
+
The `date` schema represents a calendar day, with no time and no timezone. It is stored as a `YYYY-MM-DD` string.
|
|
995
|
+
|
|
996
|
+
### Date Schema
|
|
997
|
+
|
|
998
|
+
```ts
|
|
999
|
+
s.date(); // <- Schema<string>
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Date bounds
|
|
1003
|
+
|
|
1004
|
+
Use `.from()` and `.to()` to constrain which days are valid. Both bounds are inclusive.
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
s.date().from("1900-01-01"); // this day or later
|
|
1008
|
+
s.date().to("2024-01-01"); // this day or earlier
|
|
1009
|
+
s.date().from("1900-01-01").to("2024-01-01"); // within the range
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
These are methods, not options - `s.date({ from: "1900-01-01" })` does not type check.
|
|
1013
|
+
|
|
1014
|
+
Bounds are compared as strings, which is exactly right for `YYYY-MM-DD` (it sorts chronologically) and wrong for anything else, so write bounds in that format.
|
|
1015
|
+
|
|
1016
|
+
**NOTE**: the schema checks the bounds, but not the shape of the string: a value like `"the 3rd of May"` is stored and validated without complaint. The editor always writes `YYYY-MM-DD`, so this only bites hand-written content. Use `.validate()` if you want it enforced:
|
|
1017
|
+
|
|
1018
|
+
```ts
|
|
1019
|
+
s.date().validate((day) =>
|
|
1020
|
+
/^\d{4}-\d{2}-\d{2}$/.test(day) ? false : "Must be a YYYY-MM-DD date",
|
|
1021
|
+
);
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Editing dates
|
|
1025
|
+
|
|
1026
|
+
Editors get a calendar. `from` and `to` limit which days can be picked, and a value already outside the bounds is shown clamped to the nearest one.
|
|
1027
|
+
|
|
1028
|
+
### Initializing date content
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
import { s, c } from "../val.config";
|
|
1032
|
+
|
|
1033
|
+
export const schema = s.object({
|
|
1034
|
+
birthdate: s
|
|
1035
|
+
.date()
|
|
1036
|
+
.from("1900-01-01")
|
|
1037
|
+
.to("2024-01-01")
|
|
1038
|
+
.nullable()
|
|
1039
|
+
.describe("Author's birthdate"),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
export default c.define("/content/author.val.ts", schema, {
|
|
1043
|
+
birthdate: "1981-12-30",
|
|
1044
|
+
});
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Using dates
|
|
1048
|
+
|
|
1049
|
+
The value is a plain string, so it can be compared and sorted as one:
|
|
1050
|
+
|
|
1051
|
+
```tsx
|
|
1052
|
+
const authors = [...allAuthors].sort((a, b) =>
|
|
1053
|
+
(a.birthdate ?? "").localeCompare(b.birthdate ?? ""),
|
|
1054
|
+
);
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
To format it, hand it to `Date` or a date library. Note that `new Date("2024-05-03")` parses as UTC midnight, so formatting it in a local timezone west of UTC shows the previous day - format from the parts, or use a library that treats the value as a plain day:
|
|
1058
|
+
|
|
1059
|
+
```tsx
|
|
1060
|
+
const [year, month, day] = author.birthdate.split("-").map(Number);
|
|
1061
|
+
const label = new Date(year, month - 1, day).toLocaleDateString();
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## DateTime
|
|
1065
|
+
|
|
1066
|
+
The `dateTime` schema represents an instant in time. It is stored as an ISO 8601 string in UTC, for example `2023-04-12T09:30:00.000Z`.
|
|
1067
|
+
|
|
1068
|
+
### DateTime Schema
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
s.datetime(); // <- Schema<string>
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
The factory is spelled `datetime`, all lowercase. The schema type it serializes to is `dateTime` - that name shows up in validation output and in the editor, but you never write it yourself.
|
|
1075
|
+
|
|
1076
|
+
### DateTime bounds
|
|
1077
|
+
|
|
1078
|
+
As with `date`, use the inclusive `.from()` and `.to()` methods. They accept any ISO 8601 datetime that `Date.parse` understands, and are compared as instants rather than as strings, so bounds and values may be written in different notations:
|
|
1079
|
+
|
|
1080
|
+
```ts
|
|
1081
|
+
s.datetime().from("2020-01-01T00:00:00Z");
|
|
1082
|
+
s.datetime().from("2020-01-01T00:00:00Z").to("2030-12-31T23:59:59Z");
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
Unlike `date`, the value itself is checked: a string that `Date.parse` cannot read is a validation error.
|
|
1086
|
+
|
|
1087
|
+
> Value 'yesterday' is not a valid ISO 8601 datetime
|
|
1088
|
+
|
|
1089
|
+
### Editing datetimes
|
|
1090
|
+
|
|
1091
|
+
The editor shows a calendar, a time input (down to seconds) and a timezone picker. The picker starts on the browser's timezone and remembers the last choice, so an editor in one place can enter a time as it will be experienced somewhere else. Whichever zone is chosen, the value is converted and stored as UTC - the timezone is a property of the editor, never of the content.
|
|
1092
|
+
|
|
1093
|
+
### Initializing datetime content
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
import { s, c } from "../val.config";
|
|
1097
|
+
|
|
1098
|
+
export const schema = s.object({
|
|
1099
|
+
joinedAt: s.datetime().nullable().describe("When the author joined"),
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
export default c.define("/content/author.val.ts", schema, {
|
|
1103
|
+
joinedAt: "2023-04-12T09:30:00.000Z",
|
|
1104
|
+
});
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Using datetimes
|
|
1108
|
+
|
|
1109
|
+
Since the value is an ISO 8601 UTC string, `Date` parses it directly:
|
|
1110
|
+
|
|
1111
|
+
```tsx
|
|
1112
|
+
<time dateTime={author.joinedAt}>
|
|
1113
|
+
{new Date(author.joinedAt).toLocaleString()}
|
|
1114
|
+
</time>
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
Rendering a UTC instant in the visitor's local timezone means server and client can format it differently. In Next.js that shows up as a hydration mismatch, so format inside a client component (or pass a fixed `timeZone` to `toLocaleString`) when the exact time matters.
|
|
1118
|
+
|
|
1119
|
+
**NOTE**: neither `date` nor `dateTime` values are steganographically tagged, so they do not participate in click-then-edit visual editing - edit them from the studio instead. Both support `.nullable()`, `.describe()`, `.validate()`, `.readonly()` and `.hidden()` like any other schema.
|
|
1120
|
+
|
|
889
1121
|
# Custom validation
|
|
890
1122
|
|
|
891
1123
|
All schema can use the `validate` method to show custom validation errors to editors.
|
|
@@ -37,6 +37,13 @@ export declare const Internal: {
|
|
|
37
37
|
joinModuleFilePathAndModulePath: typeof import("@valbuild/core/dist/declarations/src/module").joinModuleFilePathAndModulePath;
|
|
38
38
|
nextAppRouter: import("@valbuild/core").ValRouter;
|
|
39
39
|
externalPageRouter: import("@valbuild/core").ValRouter;
|
|
40
|
+
color: {
|
|
41
|
+
parseColor: typeof import("@valbuild/core/dist/declarations/src/schema/colorFormat").parseColor;
|
|
42
|
+
formatColor: typeof import("@valbuild/core/dist/declarations/src/schema/colorFormat").formatColor;
|
|
43
|
+
convertColor: typeof import("@valbuild/core/dist/declarations/src/schema/colorFormat").convertColor;
|
|
44
|
+
detectColorFormat: typeof import("@valbuild/core/dist/declarations/src/schema/colorFormat").detectColorFormat;
|
|
45
|
+
colorToHex: typeof import("@valbuild/core/dist/declarations/src/schema/colorFormat").colorToHex;
|
|
46
|
+
};
|
|
40
47
|
remote: {
|
|
41
48
|
createRemoteRef: typeof import("@valbuild/core/dist/declarations/src/source/remote").createRemoteRef;
|
|
42
49
|
getValidationBasis: typeof import("@valbuild/core/dist/declarations/src/remote/validationBasis").getValidationBasis;
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"next",
|
|
13
13
|
"react"
|
|
14
14
|
],
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.101.0",
|
|
16
16
|
"main": "dist/valbuild-next.cjs.js",
|
|
17
17
|
"module": "dist/valbuild-next.esm.js",
|
|
18
18
|
"exports": {
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"client-only": "^0.0.1",
|
|
49
49
|
"server-only": "^0.0.1",
|
|
50
|
-
"@valbuild/core": "0.
|
|
51
|
-
"@valbuild/language-server": "0.
|
|
52
|
-
"@valbuild/react": "0.
|
|
53
|
-
"@valbuild/server": "0.
|
|
54
|
-
"@valbuild/shared": "0.
|
|
55
|
-
"@valbuild/ui": "0.
|
|
50
|
+
"@valbuild/core": "0.101.0",
|
|
51
|
+
"@valbuild/language-server": "0.101.0",
|
|
52
|
+
"@valbuild/react": "0.101.0",
|
|
53
|
+
"@valbuild/server": "0.101.0",
|
|
54
|
+
"@valbuild/shared": "0.101.0",
|
|
55
|
+
"@valbuild/ui": "0.101.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@testing-library/react": "^14.0.0",
|