@learnpack/learnpack 5.0.359 → 5.0.360
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/lib/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/lib/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/lib/creatorDist/index.html +2 -2
- package/package.json +1 -1
- package/src/creator/src/App.test.tsx +338 -0
- package/src/creator/src/App.tsx +152 -26
- package/src/creator/src/components/CourseLanguagePicker.tsx +51 -0
- package/src/creator/src/components/LanguageDetectionModal.tsx +29 -9
- package/src/creator/src/components/StepWizard.tsx +11 -3
- package/src/creator/src/components/Uploader.test.tsx +65 -0
- package/src/creator/src/components/Uploader.tsx +9 -3
- package/src/creator/src/locales/en.json +7 -0
- package/src/creator/src/locales/es.json +7 -0
- package/src/creator/src/utils/courseLanguage.test.ts +215 -0
- package/src/creator/src/utils/courseLanguage.ts +173 -0
- package/src/creator/src/utils/creationRequest.test.ts +104 -0
- package/src/creator/src/utils/creationRequest.ts +63 -0
- package/src/creator/src/utils/store.ts +7 -0
- package/src/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/src/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/src/creatorDist/index.html +2 -2
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2105 -2105
- package/src/ui/_app/sw.js +13 -0
- package/src/ui/app.tar.gz +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { franc } from "franc";
|
|
2
|
+
|
|
3
|
+
export type TCourseLanguage = "en" | "es";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Narrows a language code to one the creator can build a course in.
|
|
7
|
+
* @param value - A code such as "es", "EN" or an i18next resolved language.
|
|
8
|
+
* @returns "en" or "es", or null for anything else (including "es-MX").
|
|
9
|
+
*/
|
|
10
|
+
export const toCourseLanguage = (
|
|
11
|
+
value: string | null | undefined
|
|
12
|
+
): TCourseLanguage | null => {
|
|
13
|
+
const code = value?.trim().toLowerCase();
|
|
14
|
+
return code === "en" || code === "es" ? code : null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// franc only reads the first 2048 characters of whatever it is given, so long
|
|
18
|
+
// text is judged window by window.
|
|
19
|
+
const WINDOW_LENGTH = 2048;
|
|
20
|
+
// Enough windows that a cover page or front matter cannot decide alone, few
|
|
21
|
+
// enough that an English bibliography at the end of a Spanish document cannot
|
|
22
|
+
// outvote its body.
|
|
23
|
+
const MAX_WINDOWS = 5;
|
|
24
|
+
// Below this many letters, titles, code and tables misclassify. Digits and
|
|
25
|
+
// symbols are not counted because they carry no language.
|
|
26
|
+
const MIN_LETTERS = 200;
|
|
27
|
+
// A window shorter than this at the end of the text is too small to vote.
|
|
28
|
+
const MIN_WINDOW_LENGTH = 200;
|
|
29
|
+
const MAJORITY = 2 / 3;
|
|
30
|
+
|
|
31
|
+
const countLetters = (text: string) => (text.match(/\p{L}/gu) || []).length;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detects whether a piece of seed text is English or Spanish.
|
|
35
|
+
*
|
|
36
|
+
* Votes over the first few 2048-character windows and only answers on a
|
|
37
|
+
* two-thirds majority, so mixed or ambiguous text comes back as null rather
|
|
38
|
+
* than as a guess. Windows in any other language vote too, so a Portuguese
|
|
39
|
+
* or French document is not passed off as Spanish or English.
|
|
40
|
+
* @param text - Text extracted from an uploaded file, pasted text or a
|
|
41
|
+
* transcript.
|
|
42
|
+
* @returns "en" or "es", "other" when the text is mostly in a language the
|
|
43
|
+
* creator does not support, or null when there is too little text or no clear
|
|
44
|
+
* majority.
|
|
45
|
+
*/
|
|
46
|
+
export const detectTextLanguage = (
|
|
47
|
+
text: string
|
|
48
|
+
): TCourseLanguage | "other" | null => {
|
|
49
|
+
if (countLetters(text) < MIN_LETTERS) return null;
|
|
50
|
+
|
|
51
|
+
let english = 0;
|
|
52
|
+
let spanish = 0;
|
|
53
|
+
let other = 0;
|
|
54
|
+
let windows = 0;
|
|
55
|
+
for (
|
|
56
|
+
let start = 0;
|
|
57
|
+
start < text.length && windows < MAX_WINDOWS;
|
|
58
|
+
start += WINDOW_LENGTH
|
|
59
|
+
) {
|
|
60
|
+
const window = text.slice(start, start + WINDOW_LENGTH);
|
|
61
|
+
if (start > 0 && window.length < MIN_WINDOW_LENGTH) continue;
|
|
62
|
+
windows++;
|
|
63
|
+
const lang = franc(window);
|
|
64
|
+
if (lang === "eng") english++;
|
|
65
|
+
else if (lang === "spa") spanish++;
|
|
66
|
+
else if (lang !== "und") other++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const votes = english + spanish + other;
|
|
70
|
+
if (votes === 0) return null;
|
|
71
|
+
if (english / votes >= MAJORITY) return "en";
|
|
72
|
+
if (spanish / votes >= MAJORITY) return "es";
|
|
73
|
+
if (other / votes >= MAJORITY) return "other";
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type TSeedLanguage = TCourseLanguage | "mixed" | null;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detects the language of everything the user gave as course content, judging
|
|
81
|
+
* each source separately so one long file cannot outvote a short one.
|
|
82
|
+
* @param seeds - The text of each uploaded file, pasted text or transcript.
|
|
83
|
+
* @returns The shared language, "mixed" when confidently detected sources
|
|
84
|
+
* disagree, or null when none could be judged or any source is in a language
|
|
85
|
+
* the creator does not support (the model then picks, as before).
|
|
86
|
+
*/
|
|
87
|
+
export const detectSeedLanguage = (seeds: string[]): TSeedLanguage => {
|
|
88
|
+
const detected = seeds.map(detectTextLanguage);
|
|
89
|
+
if (detected.includes("other")) return null;
|
|
90
|
+
const languages = new Set(
|
|
91
|
+
detected.filter((lang): lang is TCourseLanguage => lang !== null)
|
|
92
|
+
);
|
|
93
|
+
if (languages.size === 0) return null;
|
|
94
|
+
if (languages.size > 1) return "mixed";
|
|
95
|
+
return [...languages][0];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Words that exist in only one of the two languages. Ambiguous ones such as
|
|
99
|
+
// "a", "me", "no" or "son" are left out on purpose.
|
|
100
|
+
const ENGLISH_ONLY_WORDS = new Set(
|
|
101
|
+
"the to and of for with how what why when your my our this that these those is are was be into from about on in an i we you want learn teach build create make use using write explain help course lesson beginners introduction basics guide".split(
|
|
102
|
+
" "
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
const SPANISH_ONLY_WORDS = new Set(
|
|
106
|
+
"el la los las del de para con por sobre cómo como qué que y en un una unos unas mi mis nuestro nuestra nuestros es son está quiero aprender aprende enseña enseñar crear crea construir usar usando escribir explica ayúdame curso lección principiantes introducción básico básica guía desde".split(
|
|
107
|
+
" "
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
const SPANISH_CHARACTERS = /[ñáéíóú¿¡]/i;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detects the language a course description was written in.
|
|
114
|
+
*
|
|
115
|
+
* Descriptions are a few words long and full of technical terms, which
|
|
116
|
+
* statistical detection misreads ("Learn TypeScript generics" comes out as
|
|
117
|
+
* Spanish). Counting words that only exist in one language is reliable at
|
|
118
|
+
* that length, and answering only when the other language shows no sign at
|
|
119
|
+
* all keeps mixed descriptions out.
|
|
120
|
+
* @param description - The course description from the wizard.
|
|
121
|
+
* @returns "en" or "es", or null when the description is mixed or has no
|
|
122
|
+
* telling words.
|
|
123
|
+
*/
|
|
124
|
+
export const detectDescriptionLanguage = (
|
|
125
|
+
description: string
|
|
126
|
+
): TCourseLanguage | null => {
|
|
127
|
+
const words = description.toLowerCase().match(/\p{L}+/gu) || [];
|
|
128
|
+
let english = 0;
|
|
129
|
+
let spanish = 0;
|
|
130
|
+
for (const word of words) {
|
|
131
|
+
if (ENGLISH_ONLY_WORDS.has(word)) english++;
|
|
132
|
+
if (SPANISH_ONLY_WORDS.has(word)) spanish++;
|
|
133
|
+
}
|
|
134
|
+
if (SPANISH_CHARACTERS.test(description)) spanish++;
|
|
135
|
+
|
|
136
|
+
if (english > 0 && spanish === 0) return "en";
|
|
137
|
+
if (spanish > 0 && english === 0) return "es";
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type TCourseLanguageDecision =
|
|
142
|
+
// Nothing settles the language: the request goes out exactly as before and
|
|
143
|
+
// the model picks.
|
|
144
|
+
| { kind: "model" }
|
|
145
|
+
| { kind: "use"; language: TCourseLanguage }
|
|
146
|
+
// Only the user can say which language they meant.
|
|
147
|
+
| { kind: "ask" };
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decides the course language before generation.
|
|
151
|
+
*
|
|
152
|
+
* A language the user chose always wins. Otherwise the seed's language is used
|
|
153
|
+
* when the description agrees or cannot be judged, and any disagreement is put
|
|
154
|
+
* to the user. A misread description can therefore only cause a question,
|
|
155
|
+
* never a course in the wrong language.
|
|
156
|
+
*/
|
|
157
|
+
export const decideCourseLanguage = ({
|
|
158
|
+
chosen,
|
|
159
|
+
seed,
|
|
160
|
+
description,
|
|
161
|
+
}: {
|
|
162
|
+
chosen: TCourseLanguage | null;
|
|
163
|
+
seed: TSeedLanguage;
|
|
164
|
+
description: TCourseLanguage | null;
|
|
165
|
+
}): TCourseLanguageDecision => {
|
|
166
|
+
if (chosen) return { kind: "use", language: chosen };
|
|
167
|
+
if (seed === null) return { kind: "model" };
|
|
168
|
+
if (seed === "mixed") return { kind: "ask" };
|
|
169
|
+
if (description === null || description === seed) {
|
|
170
|
+
return { kind: "use", language: seed };
|
|
171
|
+
}
|
|
172
|
+
return { kind: "ask" };
|
|
173
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { ParsedFile } from "../components/FileUploader";
|
|
3
|
+
import { buildCourseInfo, toCourseFormState } from "./creationRequest";
|
|
4
|
+
import useStore, { FormState } from "./store";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The courseInfo the wizard sent before the course language could be settled,
|
|
8
|
+
* copied byte for byte from App.tsx (upstream 68c0bf9). Whitespace inside the
|
|
9
|
+
* template is part of the prompt, so it must not be reformatted.
|
|
10
|
+
*/
|
|
11
|
+
const legacyCourseInfo = (
|
|
12
|
+
formState: FormState,
|
|
13
|
+
techs: { slug: string }[],
|
|
14
|
+
uploadedFiles: ParsedFile[]
|
|
15
|
+
) =>
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
`${JSON.stringify(
|
|
18
|
+
formState
|
|
19
|
+
)}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
|
|
20
|
+
.map((t) => t.slug)
|
|
21
|
+
.join(", ")}</techs>
|
|
22
|
+
|
|
23
|
+
The following files have been uploaded by the user: ${uploadedFiles.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`).join("\n")}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const defaultFormState = () => useStore.getInitialState().formState;
|
|
27
|
+
|
|
28
|
+
const techs = [{ slug: "python" }, { slug: "sql" }];
|
|
29
|
+
const uploadedFiles: ParsedFile[] = [
|
|
30
|
+
{
|
|
31
|
+
name: "notes.pdf",
|
|
32
|
+
text: "Relational databases",
|
|
33
|
+
status: "SUCCESS",
|
|
34
|
+
notificationId: "n1",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe("buildCourseInfo", () => {
|
|
39
|
+
it("sends exactly the previous prompt when no language is settled", () => {
|
|
40
|
+
const formState = {
|
|
41
|
+
...defaultFormState(),
|
|
42
|
+
description: "Learn SQL joins",
|
|
43
|
+
purpose: "learnpack-lesson-writer",
|
|
44
|
+
duration: 30,
|
|
45
|
+
hasContentIndex: true,
|
|
46
|
+
};
|
|
47
|
+
// The wizard state before this change had no languageChosen key.
|
|
48
|
+
const previousFormState = { ...formState };
|
|
49
|
+
delete previousFormState.languageChosen;
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
buildCourseInfo({ formState, techs, uploadedFiles, language: null })
|
|
53
|
+
).toBe(legacyCourseInfo(previousFormState, techs, uploadedFiles));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("asks for the settled language in plain words", () => {
|
|
57
|
+
const formState = { ...defaultFormState(), language: "en" };
|
|
58
|
+
const courseInfo = buildCourseInfo({
|
|
59
|
+
formState,
|
|
60
|
+
techs,
|
|
61
|
+
uploadedFiles,
|
|
62
|
+
language: "es",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(courseInfo).toContain('"language":"es"');
|
|
66
|
+
expect(courseInfo).not.toContain('"language":"en"');
|
|
67
|
+
expect(
|
|
68
|
+
courseInfo.endsWith(
|
|
69
|
+
"The user chose Spanish as the course language. This replaces the rule to write in the language of the user messages: write every field in Spanish, including the title, the description, and each step's title and description in listOfSteps, even if the user's messages or the uploaded files are in another language. Set languageCode to \"es\"."
|
|
70
|
+
)
|
|
71
|
+
).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("never sends languageChosen to the model", () => {
|
|
75
|
+
const formState = {
|
|
76
|
+
...defaultFormState(),
|
|
77
|
+
language: "es",
|
|
78
|
+
languageChosen: true,
|
|
79
|
+
};
|
|
80
|
+
for (const language of [null, "es"] as const) {
|
|
81
|
+
expect(
|
|
82
|
+
buildCourseInfo({ formState, techs, uploadedFiles, language })
|
|
83
|
+
).not.toContain("languageChosen");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("toCourseFormState", () => {
|
|
89
|
+
it("drops languageChosen and keeps every other key in order", () => {
|
|
90
|
+
const formState = { ...defaultFormState(), languageChosen: true };
|
|
91
|
+
const courseFormState = toCourseFormState(formState);
|
|
92
|
+
|
|
93
|
+
expect("languageChosen" in courseFormState).toBe(false);
|
|
94
|
+
expect(Object.keys(courseFormState)).toEqual(
|
|
95
|
+
Object.keys(formState).filter((key) => key !== "languageChosen")
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not change the form state it was given", () => {
|
|
100
|
+
const formState = { ...defaultFormState(), languageChosen: true };
|
|
101
|
+
toCourseFormState(formState);
|
|
102
|
+
expect(formState.languageChosen).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ParsedFile } from "../components/FileUploader";
|
|
2
|
+
import type { TCourseLanguage } from "./courseLanguage";
|
|
3
|
+
import type { FormState } from "./store";
|
|
4
|
+
|
|
5
|
+
const LANGUAGE_NAMES: Record<TCourseLanguage, string> = {
|
|
6
|
+
en: "English",
|
|
7
|
+
es: "Spanish",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drops the wizard-only fields from the form state before it leaves the
|
|
12
|
+
* wizard, in the prompt or in the syllabus sent to the server.
|
|
13
|
+
* @param formState - The wizard's form state.
|
|
14
|
+
* @returns A copy without `languageChosen`, keys otherwise in the same order.
|
|
15
|
+
*/
|
|
16
|
+
export const toCourseFormState = (formState: FormState): FormState => {
|
|
17
|
+
const courseFormState = { ...formState };
|
|
18
|
+
delete courseFormState.languageChosen;
|
|
19
|
+
return courseFormState;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the `courseInfo` text sent to the model to generate the syllabus.
|
|
24
|
+
*
|
|
25
|
+
* Without a settled language the text is exactly what the wizard sent before
|
|
26
|
+
* the course language could be settled, so the model keeps choosing.
|
|
27
|
+
* @param language - The course language settled before generation, or null.
|
|
28
|
+
*/
|
|
29
|
+
export const buildCourseInfo = ({
|
|
30
|
+
formState,
|
|
31
|
+
techs,
|
|
32
|
+
uploadedFiles,
|
|
33
|
+
language,
|
|
34
|
+
}: {
|
|
35
|
+
formState: FormState;
|
|
36
|
+
techs: { slug: string }[];
|
|
37
|
+
uploadedFiles: ParsedFile[];
|
|
38
|
+
language: TCourseLanguage | null;
|
|
39
|
+
}): string => {
|
|
40
|
+
const courseFormState = toCourseFormState(formState);
|
|
41
|
+
|
|
42
|
+
// Kept byte for byte from the original request, including the spaces on the
|
|
43
|
+
// blank line, which .editorconfig would trim. creationRequest.test.ts checks
|
|
44
|
+
// it against a copy of the original.
|
|
45
|
+
const courseInfo = `${JSON.stringify(
|
|
46
|
+
language ? { ...courseFormState, language } : courseFormState
|
|
47
|
+
)}. The following technologies are available, choose up to 3 from the following list: <techs>${techs
|
|
48
|
+
.map((t) => t.slug)
|
|
49
|
+
.join(", ")}</techs>
|
|
50
|
+
|
|
51
|
+
The following files have been uploaded by the user: ${uploadedFiles
|
|
52
|
+
.map((f) => `<FILE name="${f.name}">${f.text}</FILE>`)
|
|
53
|
+
.join("\n")}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
if (!language) return courseInfo;
|
|
57
|
+
|
|
58
|
+
const name = LANGUAGE_NAMES[language];
|
|
59
|
+
// Prompt 390 keys on the first sentence to override its rule of writing in
|
|
60
|
+
// the language of the user messages, so keep its wording in sync.
|
|
61
|
+
return `${courseInfo}
|
|
62
|
+
The user chose ${name} as the course language. This replaces the rule to write in the language of the user messages: write every field in ${name}, including the title, the description, and each step's title and description in listOfSteps, even if the user's messages or the uploaded files are in another language. Set languageCode to "${language}".`;
|
|
63
|
+
};
|
|
@@ -15,6 +15,10 @@ export type FormState = {
|
|
|
15
15
|
difficulty: TDifficulty;
|
|
16
16
|
slug: string;
|
|
17
17
|
language?: string;
|
|
18
|
+
// True once the user picked the course language themselves (the language
|
|
19
|
+
// picker, the `language` URL param or a language modal). Never sent to the
|
|
20
|
+
// model or the server.
|
|
21
|
+
languageChosen?: boolean;
|
|
18
22
|
isCompleted: boolean;
|
|
19
23
|
variables: string[];
|
|
20
24
|
currentStep: string;
|
|
@@ -93,6 +97,7 @@ const useStore = create<Store>()(
|
|
|
93
97
|
contentIndex: "",
|
|
94
98
|
purpose: "",
|
|
95
99
|
language: "en",
|
|
100
|
+
languageChosen: false,
|
|
96
101
|
technologies: [],
|
|
97
102
|
difficulty: "beginner",
|
|
98
103
|
// sources: [],
|
|
@@ -129,6 +134,7 @@ const useStore = create<Store>()(
|
|
|
129
134
|
description: "",
|
|
130
135
|
duration: 0,
|
|
131
136
|
language: "en",
|
|
137
|
+
languageChosen: false,
|
|
132
138
|
difficulty: "beginner",
|
|
133
139
|
technologies: [],
|
|
134
140
|
hasContentIndex: false,
|
|
@@ -179,6 +185,7 @@ const useStore = create<Store>()(
|
|
|
179
185
|
slug: "",
|
|
180
186
|
difficulty: "beginner",
|
|
181
187
|
language: "en",
|
|
188
|
+
languageChosen: false,
|
|
182
189
|
technologies: [],
|
|
183
190
|
hasContentIndex: false,
|
|
184
191
|
contentIndex: "",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
--color-blue-50: oklch(97% 0.014 254.604);
|
|
66
66
|
--color-blue-100: oklch(93.2% 0.032 255.585);
|
|
67
67
|
--color-blue-200: oklch(88.2% 0.059 254.128);
|
|
68
|
+
--color-blue-300: oklch(80.9% 0.105 251.813);
|
|
68
69
|
--color-blue-400: oklch(70.7% 0.165 254.624);
|
|
69
70
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
|
70
71
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
|
@@ -439,6 +440,12 @@
|
|
|
439
440
|
.z-1000 {
|
|
440
441
|
z-index: 1000;
|
|
441
442
|
}
|
|
443
|
+
.col-span-2 {
|
|
444
|
+
grid-column: span 2 / span 2;
|
|
445
|
+
}
|
|
446
|
+
.row-start-2 {
|
|
447
|
+
grid-row-start: 2;
|
|
448
|
+
}
|
|
442
449
|
.container {
|
|
443
450
|
width: 100%;
|
|
444
451
|
}
|
|
@@ -536,6 +543,9 @@
|
|
|
536
543
|
.flex {
|
|
537
544
|
display: flex;
|
|
538
545
|
}
|
|
546
|
+
.grid {
|
|
547
|
+
display: grid;
|
|
548
|
+
}
|
|
539
549
|
.hidden {
|
|
540
550
|
display: none;
|
|
541
551
|
}
|
|
@@ -548,6 +558,9 @@
|
|
|
548
558
|
.table {
|
|
549
559
|
display: table;
|
|
550
560
|
}
|
|
561
|
+
.field-sizing-content {
|
|
562
|
+
field-sizing: content;
|
|
563
|
+
}
|
|
551
564
|
.h-2 {
|
|
552
565
|
height: calc(var(--spacing) * 2);
|
|
553
566
|
}
|
|
@@ -644,6 +657,9 @@
|
|
|
644
657
|
.w-full {
|
|
645
658
|
width: 100%;
|
|
646
659
|
}
|
|
660
|
+
.max-w-64 {
|
|
661
|
+
max-width: calc(var(--spacing) * 64);
|
|
662
|
+
}
|
|
647
663
|
.max-w-\[700px\] {
|
|
648
664
|
max-width: 700px;
|
|
649
665
|
}
|
|
@@ -705,6 +721,9 @@
|
|
|
705
721
|
.list-disc {
|
|
706
722
|
list-style-type: disc;
|
|
707
723
|
}
|
|
724
|
+
.grid-cols-2 {
|
|
725
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
726
|
+
}
|
|
708
727
|
.flex-col {
|
|
709
728
|
flex-direction: column;
|
|
710
729
|
}
|
|
@@ -780,6 +799,15 @@
|
|
|
780
799
|
calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))
|
|
781
800
|
);
|
|
782
801
|
}
|
|
802
|
+
.justify-self-center {
|
|
803
|
+
justify-self: center;
|
|
804
|
+
}
|
|
805
|
+
.justify-self-end {
|
|
806
|
+
justify-self: flex-end;
|
|
807
|
+
}
|
|
808
|
+
.justify-self-start {
|
|
809
|
+
justify-self: flex-start;
|
|
810
|
+
}
|
|
783
811
|
.truncate {
|
|
784
812
|
text-overflow: ellipsis;
|
|
785
813
|
white-space: nowrap;
|
|
@@ -905,6 +933,9 @@
|
|
|
905
933
|
.bg-red-100 {
|
|
906
934
|
background-color: var(--color-red-100);
|
|
907
935
|
}
|
|
936
|
+
.bg-transparent {
|
|
937
|
+
background-color: #0000;
|
|
938
|
+
}
|
|
908
939
|
.bg-white {
|
|
909
940
|
background-color: var(--color-white);
|
|
910
941
|
}
|
|
@@ -1046,6 +1077,9 @@
|
|
|
1046
1077
|
.break-words {
|
|
1047
1078
|
overflow-wrap: break-word;
|
|
1048
1079
|
}
|
|
1080
|
+
.whitespace-nowrap {
|
|
1081
|
+
white-space: nowrap;
|
|
1082
|
+
}
|
|
1049
1083
|
.whitespace-pre-line {
|
|
1050
1084
|
white-space: pre-line;
|
|
1051
1085
|
}
|
|
@@ -1244,6 +1278,19 @@
|
|
|
1244
1278
|
.focus\:ring-blue-200:focus {
|
|
1245
1279
|
--tw-ring-color: var(--color-blue-200);
|
|
1246
1280
|
}
|
|
1281
|
+
.focus\:outline-none:focus {
|
|
1282
|
+
--tw-outline-style: none;
|
|
1283
|
+
outline-style: none;
|
|
1284
|
+
}
|
|
1285
|
+
.focus-visible\:ring-2:focus-visible {
|
|
1286
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
|
|
1287
|
+
calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
1288
|
+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
|
|
1289
|
+
var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1290
|
+
}
|
|
1291
|
+
.focus-visible\:ring-blue-300:focus-visible {
|
|
1292
|
+
--tw-ring-color: var(--color-blue-300);
|
|
1293
|
+
}
|
|
1247
1294
|
.disabled\:cursor-not-allowed:disabled {
|
|
1248
1295
|
cursor: not-allowed;
|
|
1249
1296
|
}
|
|
@@ -1257,9 +1304,21 @@
|
|
|
1257
1304
|
opacity: 0.5;
|
|
1258
1305
|
}
|
|
1259
1306
|
@media (min-width: 40rem) {
|
|
1307
|
+
.sm\:col-span-1 {
|
|
1308
|
+
grid-column: span 1 / span 1;
|
|
1309
|
+
}
|
|
1310
|
+
.sm\:col-start-2 {
|
|
1311
|
+
grid-column-start: 2;
|
|
1312
|
+
}
|
|
1313
|
+
.sm\:row-start-1 {
|
|
1314
|
+
grid-row-start: 1;
|
|
1315
|
+
}
|
|
1260
1316
|
.sm\:w-auto {
|
|
1261
1317
|
width: auto;
|
|
1262
1318
|
}
|
|
1319
|
+
.sm\:grid-cols-\[1fr_auto_1fr\] {
|
|
1320
|
+
grid-template-columns: 1fr auto 1fr;
|
|
1321
|
+
}
|
|
1263
1322
|
.sm\:flex-row {
|
|
1264
1323
|
flex-direction: row;
|
|
1265
1324
|
}
|