fix lint issues with prettier

This commit is contained in:
2025-03-27 22:08:01 +01:00
parent 03120d7242
commit a56454110a
45 changed files with 2766 additions and 2672 deletions

View File

@@ -9,16 +9,24 @@
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@plugin "daisyui" {
themes: light --default, dark --prefersdark, light, dark, cyberpunk, synthwave, retro, coffee, dracula;
themes:
light --default,
dark --prefersdark,
light,
dark,
cyberpunk,
synthwave,
retro,
coffee,
dracula;
}

24
apps/app/src/app.d.ts vendored
View File

@@ -3,18 +3,18 @@ import { KVNamespace } from '@cloudflare/workers-types';
// for information about these interfaces
declare global {
namespace App {
interface Locals {
session: Session | null;
}
interface Platform {
env: {
GH_MEDIA: R2Bucket;
interface Locals {
session: Session | null;
}
interface Platform {
env: {
GH_MEDIA: R2Bucket;
GH_SESSIONS: KVNamespace;
};
cf: CfProperties
ctx: ExecutionContext
}
}
};
cf: CfProperties;
ctx: ExecutionContext;
}
}
}
export {};
export {};

View File

@@ -1,47 +1,51 @@
import type { Handle } from '@sveltejs/kit';
import { themes } from '$lib/themes'
import { themes } from '$lib/themes';
import { i18n } from '$lib/i18n';
import { validateSessionToken, setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/server/session";
import { sequence } from "@sveltejs/kit/hooks";
import {
validateSessionToken,
setSessionTokenCookie,
deleteSessionTokenCookie
} from '$lib/server/session';
import { sequence } from '@sveltejs/kit/hooks';
const handleParaglide: Handle = i18n.handle();
const authHandle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("session") ?? null;
if (token === null) {
event.locals.session = null;
return resolve(event);
}
const token = event.cookies.get('session') ?? null;
if (token === null) {
event.locals.session = null;
return resolve(event);
}
if(!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS){
return new Response("Server configuration error. GH_SESSIONS KeyValue store missing", {
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
return new Response('Server configuration error. GH_SESSIONS KeyValue store missing', {
status: 500
});
}
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
if (session !== null) {
setSessionTokenCookie(event, token, session.expiresAt);
} else {
deleteSessionTokenCookie(event);
}
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
if (session !== null) {
setSessionTokenCookie(event, token, session.expiresAt);
} else {
deleteSessionTokenCookie(event);
}
event.locals.session = session;
return resolve(event);
event.locals.session = session;
return resolve(event);
};
const themeHandler: Handle = async ({ event, resolve }) => {
const theme = event.cookies.get('theme')
const theme = event.cookies.get('theme');
if (!theme || !themes.includes(theme)) {
return await resolve(event)
return await resolve(event);
}
return await resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace('data-theme=""', `data-theme="${theme}"`)
},
})
}
return html.replace('data-theme=""', `data-theme="${theme}"`);
}
});
};
export const handle: Handle = sequence(handleParaglide, authHandle,themeHandler);
export const handle: Handle = sequence(handleParaglide, authHandle, themeHandler);

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,20 @@
<div role="alert" class="alert alert-vertical sm:alert-horizontal">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span></span>
<div>
<button class="btn btn-sm">Deny</button>
<button class="btn btn-sm btn-primary">Accept</button>
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span></span>
<div>
<button class="btn btn-sm">Deny</button>
<button class="btn btn-sm btn-primary">Accept</button>
</div>
</div>

View File

@@ -2,64 +2,64 @@ import { Node, Relationship, Date } from 'neo4j-driver';
import { Integer } from 'neo4j-driver';
export interface PersonProperties {
allow_admin_access: boolean;
google_id: string;
first_name: string;
middle_name?: string;
last_name: string;
titles?: string[]; // e.g. Jr., Sr., III
suffixes?: string[]; // e.g. Ph.D., M.D.
extra_names?: string[];
aliases?: string[];
mothers_first_name?: string;
mothers_last_name?: string;
born?: Date<number>;
place_of_birth?: string;
died?: Date<number>;
place_of_death?: string;
life_events?: { [key: string]: {from: Date<number>, to:Date<number>, desription: string} }[];
occupations?: string[];
occupation_to_display?: string;
others_said?: { [key: string]: string };
limit: number;
photos?: { [key: string]: string };
videos?: { [key: string]: string };
audios?: { [key: string]: string };
profile_picture?: string;
verified: boolean;
email?: string;
phone?: string;
residence?: {
city?: string;
country?: string;
zip_code?: string;
address_line_1?: string;
address_line_2?: string;
};
religion?: string;
baptized?: string;
ideology?: string;
blood_type?: string;
allergies?: string[];
medications?: string[];
medical_conditions?: string[];
height?: number;
weight?: number;
hair_colour?: string;
skin_colour?: string;
eye_colour?: string;
sports?: string[];
hobbies?: string[];
interests?: string[];
languages?: { [key: string]: string };
notes?: string;
allow_admin_access: boolean;
google_id: string;
first_name: string;
middle_name?: string;
last_name: string;
titles?: string[]; // e.g. Jr., Sr., III
suffixes?: string[]; // e.g. Ph.D., M.D.
extra_names?: string[];
aliases?: string[];
mothers_first_name?: string;
mothers_last_name?: string;
born?: Date<number>;
place_of_birth?: string;
died?: Date<number>;
place_of_death?: string;
life_events?: { [key: string]: { from: Date<number>; to: Date<number>; desription: string } }[];
occupations?: string[];
occupation_to_display?: string;
others_said?: { [key: string]: string };
limit: number;
photos?: { [key: string]: string };
videos?: { [key: string]: string };
audios?: { [key: string]: string };
profile_picture?: string;
verified: boolean;
email?: string;
phone?: string;
residence?: {
city?: string;
country?: string;
zip_code?: string;
address_line_1?: string;
address_line_2?: string;
};
religion?: string;
baptized?: string;
ideology?: string;
blood_type?: string;
allergies?: string[];
medications?: string[];
medical_conditions?: string[];
height?: number;
weight?: number;
hair_colour?: string;
skin_colour?: string;
eye_colour?: string;
sports?: string[];
hobbies?: string[];
interests?: string[];
languages?: { [key: string]: string };
notes?: string;
}
export interface FamilyRelationship {
verified: boolean;
notes?: string;
from?: Date<number>;
to?: Date<number>;
verified: boolean;
notes?: string;
from?: Date<number>;
to?: Date<number>;
}
export type Person = Node<Integer, PersonProperties>;
@@ -69,36 +69,38 @@ export type Spouse = Relationship<Integer, FamilyRelationship>;
export type Child = Relationship<Integer, FamilyRelationship>;
export interface RecipeProperties {
id: string;
name: string;
origin: string;
category: string;
first_recorded: Date<number>;
description: string;
ingredients: string[];
instructions: string[];
photo: string;
notes?: string;
id: string;
name: string;
origin: string;
category: string;
first_recorded: Date<number>;
description: string;
ingredients: string[];
instructions: string[];
photo: string;
notes?: string;
}
export type Recipe = Node<Integer, RecipeProperties>;
export type Likes = Relationship<Integer, {
favourite: boolean;
like_it: boolean;
could_make_it: boolean;
}>;
export type Likes = Relationship<
Integer,
{
favourite: boolean;
like_it: boolean;
could_make_it: boolean;
}
>;
export interface FamilyTree {
ancestors: String;
prel1: FamilyRelationship;
children: String;
prel2: FamilyRelationship;
spouses: String;
srel: FamilyRelationship;
user: String;
ancestors: String;
prel1: FamilyRelationship;
children: String;
prel2: FamilyRelationship;
spouses: String;
srel: FamilyRelationship;
user: String;
}
export interface FamilyMember {
person: Person;
}
person: Person;
}

View File

@@ -1,4 +1,8 @@
import { Google } from "arctic";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from "$env/static/private";
import { Google } from 'arctic';
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from '$env/static/private';
export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI || "http://localhost:5173/login/google/callback");
export const google = new Google(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_CALLBACK_URI || 'http://localhost:5173/login/google/callback'
);

View File

@@ -1,15 +1,18 @@
import type { KVNamespace } from "@cloudflare/workers-types";
import { encodeBase32, encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import type { KVNamespace } from '@cloudflare/workers-types';
import { encodeBase32, encodeHexLowerCase } from '@oslojs/encoding';
import { sha256 } from '@oslojs/crypto/sha2';
import type { RequestEvent } from "@sveltejs/kit";
import type { RequestEvent } from '@sveltejs/kit';
// in seconds
const EXPIRATION_TTL: number = 60 * 60 * 24 * 7;
export async function validateSessionToken(token: string, sessions: KVNamespace): Promise<SessionValidationResult> {
export async function validateSessionToken(
token: string,
sessions: KVNamespace
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session | null = await sessions.get(sessionId, { type: "json" });
const session: Session | null = await sessions.get(sessionId, { type: 'json' });
if (!session) {
return null;
@@ -33,21 +36,21 @@ export async function invalidateUserSessions(userId: number, sessions: KVNamespa
}
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void {
event.cookies.set("session", token, {
event.cookies.set('session', token, {
httpOnly: true,
path: "/",
path: '/',
secure: import.meta.env.PROD,
sameSite: "lax",
sameSite: 'lax',
expires: expiresAt
});
}
export function deleteSessionTokenCookie(event: RequestEvent): void {
event.cookies.set("session", "", {
event.cookies.set('session', '', {
httpOnly: true,
path: "/",
path: '/',
secure: import.meta.env.PROD,
sameSite: "lax",
sameSite: 'lax',
maxAge: 0
});
}
@@ -59,7 +62,11 @@ export function generateSessionToken(): string {
return token;
}
export async function createSession(token: string, userId: string, sessions: KVNamespace): Promise<Session> {
export async function createSession(
token: string,
userId: string,
sessions: KVNamespace
): Promise<Session> {
const sessionId = `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`;
const session: Session = {
id: sessionId,

View File

@@ -4,37 +4,37 @@ import { i18n } from '$lib/i18n';
import { goto } from '$app/navigation';
vi.mock('$lib/i18n', () => ({
i18n: {
route: vi.fn().mockImplementation((translatedPath: string) => ''),
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
}
i18n: {
route: vi.fn().mockImplementation((translatedPath: string) => ''),
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
}
}));
vi.mock('$app/state', () => ({
page: {
url: {
pathname: '/current-path'
}
}
page: {
url: {
pathname: '/current-path'
}
}
}));
vi.mock('$app/navigation', () => ({
goto: vi.fn()
goto: vi.fn()
}));
describe('switchToLanguage', () => {
it('should switch to the new language', () => {
const newLanguage = 'en';
const canonicalPath = '/canonical-path';
const localisedPath = '/en/canonical-path';
it('should switch to the new language', () => {
const newLanguage = 'en';
const canonicalPath = '/canonical-path';
const localisedPath = '/en/canonical-path';
i18n.route.mockReturnValue(canonicalPath);
i18n.resolveRoute.mockReturnValue(localisedPath);
i18n.route.mockReturnValue(canonicalPath);
i18n.resolveRoute.mockReturnValue(localisedPath);
switchToLanguage(newLanguage);
switchToLanguage(newLanguage);
expect(i18n.route).toHaveBeenCalledWith('/current-path');
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
expect(goto).toHaveBeenCalledWith(localisedPath);
});
});
expect(i18n.route).toHaveBeenCalledWith('/current-path');
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
expect(goto).toHaveBeenCalledWith(localisedPath);
});
});

View File

@@ -4,7 +4,7 @@ import { page } from '$app/state';
import { goto } from '$app/navigation';
export function switchToLanguage(newLanguage: AvailableLanguageTag) {
const canonicalPath = i18n.route(page.url.pathname);
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
goto(localisedPath);
}
const canonicalPath = i18n.route(page.url.pathname);
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
goto(localisedPath);
}

View File

@@ -37,7 +37,7 @@
}
</script>
<div class="dropdown dropdown-end block ">
<div class="dropdown dropdown-end block">
<select
bind:value={current_theme}
data-choose-theme
@@ -48,11 +48,7 @@
{theme()}
</option>
{#each themes as theme}
<option
value={theme}
class="theme-controller capitalize"
>{themeMessages.get(theme)}</option
>
<option value={theme} class="theme-controller capitalize">{themeMessages.get(theme)}</option>
{/each}
</select>
</div>

View File

@@ -1 +1 @@
export const themes = ['light', 'dark','coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
export const themes = ['light', 'dark', 'coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];

View File

@@ -1,10 +1,10 @@
import { fail, redirect } from "@sveltejs/kit";
import { deleteSessionTokenCookie, invalidateSession } from "$lib/server/session";
import type { Actions, RequestEvent } from "./$types";
import { fail, redirect } from '@sveltejs/kit';
import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/session';
import type { Actions, RequestEvent } from './$types';
export async function load(event: RequestEvent) {
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
return redirect(302, "/login");
return redirect(302, '/login');
}
return {
// TODO - Add Family Graph
@@ -19,14 +19,14 @@ async function logout(event: RequestEvent) {
if (event.locals.session === null) {
return fail(401);
}
if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) {
invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS);
} else {
return fail(500, { message: "Server configuration error" });
return fail(500, { message: 'Server configuration error' });
}
deleteSessionTokenCookie(event);
return redirect(302, "/login");
}
return redirect(302, '/login');
}

View File

@@ -1,10 +1,17 @@
<script lang="ts">
import {title, family_tree} from '$lib/paraglide/messages.js';
import { SvelteFlowProvider,Background, BackgroundVariant, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
import { title, family_tree } from '$lib/paraglide/messages.js';
import {
SvelteFlowProvider,
Background,
BackgroundVariant,
SvelteFlow,
Controls,
MiniMap
} from '@xyflow/svelte';
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
let nodes = $state.raw<Node[]>([]);
let edges = $state.raw<Edge[]>([]);
let nodes = $state.raw<Node[]>([]);
let edges = $state.raw<Edge[]>([]);
</script>
<svelte:head>
@@ -19,4 +26,3 @@
</SvelteFlow>
</SvelteFlowProvider>
</div>

View File

@@ -1,11 +1,11 @@
import { redirect } from "@sveltejs/kit";
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from "./$types";
import type { RequestEvent } from './$types';
export async function load(event: RequestEvent) {
if (event.locals.session !== null) {
return redirect(302, "/");
return redirect(302, '/');
}
return {};
}
}

View File

@@ -17,7 +17,7 @@
<p class="py-6">
{site_intro()}
</p>
<a href="/login/google" class="btn rounded-full bg-white text-black border-[#e5e5e5]">
<a href="/login/google" class="btn rounded-full border-[#e5e5e5] bg-white text-black">
<!-- Google -->
<svg
aria-label="Google logo"

View File

@@ -1,26 +1,26 @@
import { google } from "$lib/server/oauth";
import { generateCodeVerifier, generateState } from "arctic";
import { google } from '$lib/server/oauth';
import { generateCodeVerifier, generateState } from 'arctic';
import type { RequestEvent } from "./$types";
import type { RequestEvent } from './$types';
export function GET(event: RequestEvent): Response {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile", "email"]);
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']);
event.cookies.set("google_oauth_state", state, {
event.cookies.set('google_oauth_state', state, {
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD,
path: "/",
sameSite: "lax"
path: '/',
sameSite: 'lax'
});
event.cookies.set("google_code_verifier", codeVerifier, {
event.cookies.set('google_code_verifier', codeVerifier, {
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD,
path: "/",
sameSite: "lax"
path: '/',
sameSite: 'lax'
});
return new Response(null, {
@@ -29,4 +29,4 @@ export function GET(event: RequestEvent): Response {
Location: url.toString()
}
});
}
}

View File

@@ -1,43 +1,51 @@
import { google } from "$lib/server/oauth";
import { ObjectParser } from "@pilcrowjs/object-parser";
import { createUser, getUserFromGoogleId } from "$lib/server/user";
import { DB } from "$lib/server/db";
import { google } from '$lib/server/oauth';
import { ObjectParser } from '@pilcrowjs/object-parser';
import { createUser, getUserFromGoogleId } from '$lib/server/user';
import { DB } from '$lib/server/db';
import { browser } from '$app/environment';
import { Date as neoDate } from 'neo4j-driver';
import { createSession, generateSessionToken, setSessionTokenCookie } from "$lib/server/session";
import { decodeIdToken } from "arctic";
import { missing_field, last_name, first_name, mothers_first_name, mothers_last_name, born, failed_to_create_user } from "$lib/paraglide/messages";
import { createSession, generateSessionToken, setSessionTokenCookie } from '$lib/server/session';
import { decodeIdToken } from 'arctic';
import {
missing_field,
last_name,
first_name,
mothers_first_name,
mothers_last_name,
born,
failed_to_create_user
} from '$lib/paraglide/messages';
import type { PageServerLoad, Actions, RequestEvent, PageData } from "./$types";
import type { OAuth2Tokens } from "arctic";
import type { PageServerLoad, Actions, RequestEvent, PageData } from './$types';
import type { OAuth2Tokens } from 'arctic';
import type { PersonProperties } from '$lib/model';
import { error, redirect, fail } from "@sveltejs/kit";
import { error, redirect, fail } from '@sveltejs/kit';
const StorageLimit = 200 * 1024 * 1024;
export const load: PageServerLoad = async (event: RequestEvent) => {
//prevent loading in developer mode, due to some issues with universal load, even if this is a server only ts,it will still run on client in dev mode idk
if (browser) {
return {}
if (browser) {
return {};
}
const storedState = event.cookies.get("google_oauth_state") ?? null;
const codeVerifier = event.cookies.get("google_code_verifier") ?? null;
const code = event.url.searchParams.get("code");
const state = event.url.searchParams.get("state");
const storedState = event.cookies.get('google_oauth_state') ?? null;
const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state');
if (storedState === null || codeVerifier === null || code === null || state === null) {
return error(400, { message: "Please restart the process." })
return error(400, { message: 'Please restart the process.' });
}
if (storedState !== state) {
return error(400, { message: "Please restart the process." })
return error(400, { message: 'Please restart the process.' });
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
return error(400, { message: "Failed to validate authorization code with " + e });
return error(400, { message: 'Failed to validate authorization code with ' + e });
}
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
@@ -47,10 +55,10 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
const claims = decodeIdToken(tokens.idToken());
const claimsParser = new ObjectParser(claims);
const sub = claimsParser.getString("sub");
const family_name = claimsParser.getString("family_name");
const first_name = claimsParser.getString("given_name");
const email = claimsParser.getString("email");
const sub = claimsParser.getString('sub');
const family_name = claimsParser.getString('family_name');
const first_name = claimsParser.getString('given_name');
const email = claimsParser.getString('email');
const dbSession = DB.session();
const existingUser = await getUserFromGoogleId(dbSession, sub);
@@ -62,7 +70,7 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
// const session = await createSession(sessionToken, eUser.get('elementId'), event.platform.env.GH_SESSIONS);
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, "/");
return redirect(302, '/');
}
let personP: PersonProperties = {
@@ -72,13 +80,13 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
email: email,
allow_admin_access: false,
limit: StorageLimit,
verified: false,
verified: false
};
return {
props: personP
};
}
};
export const actions: Actions = {
register: register
@@ -89,15 +97,15 @@ async function register(event: RequestEvent) {
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
// return fail(500, { message: "Server configuration error. GH_SESSIONS KeyValue store missing" });
// }
const google_id = data.get('google_id')
const google_id = data.get('google_id');
if (google_id === null) {
return fail(400, {
message: missing_field({
field: "google_id"
field: 'google_id'
})
});
}
const first_name_f = data.get('first_name')
const first_name_f = data.get('first_name');
if (first_name_f === null) {
return fail(400, {
message: missing_field({
@@ -105,49 +113,44 @@ async function register(event: RequestEvent) {
})
});
}
const last_name_f = data.get('last_name')
const last_name_f = data.get('last_name');
if (last_name_f === null) {
return fail(400, {
message:
missing_field({
field: last_name()
})
message: missing_field({
field: last_name()
})
});
}
const email = data.get('email')
const email = data.get('email');
if (email === null) {
return fail(400, {
message:
missing_field({
field: "Email"
})
message: missing_field({
field: 'Email'
})
});
}
const birth_date = data.get('birth_date');
if (birth_date === null) {
return fail(400, {
message:
missing_field({
field: born()
})
message: missing_field({
field: born()
})
});
}
const mothers_first_name_f = data.get('mothers_first_name');
if (mothers_first_name_f === null) {
return fail(400, {
message:
missing_field({
field: mothers_first_name()
})
message: missing_field({
field: mothers_first_name()
})
});
}
const mothers_last_name_f = data.get('mothers_last_name');
if (mothers_last_name_f === null) {
return fail(400, {
message:
missing_field({
field: mothers_last_name()
})
message: missing_field({
field: mothers_last_name()
})
});
}
@@ -158,12 +161,16 @@ async function register(event: RequestEvent) {
first_name: first_name_f as string,
last_name: last_name_f as string,
email: email as string,
born: new neoDate(parsed_date.getFullYear(), parsed_date.getUTCMonth(), parsed_date.getUTCDate()),
born: new neoDate(
parsed_date.getFullYear(),
parsed_date.getUTCMonth(),
parsed_date.getUTCDate()
),
mothers_first_name: mothers_first_name_f as string,
mothers_last_name: mothers_last_name_f as string,
allow_admin_access: false,
limit: StorageLimit,
verified: false,
verified: false
};
const dbSession = DB.session();
@@ -179,5 +186,5 @@ async function register(event: RequestEvent) {
// const session = await createSession(sessionToken, user.get('elementId'), event.platform.env.GH_SESSIONS);
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, "/");
}
return redirect(302, '/');
}

View File

@@ -12,7 +12,7 @@
last_name,
first_name,
email,
allow_family_tree_admin_access,
allow_family_tree_admin_access
} from '$lib/paraglide/messages';
import FamilyTree from '../../highresolution_icon_no_background_croped.png';
let { data, form }: PageProps = $props();
@@ -49,24 +49,65 @@
<form method="POST" action="/register">
<fieldset class="fieldset">
{#if form?.message}
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{form.message}</span>
</div>
{/if }
<div role="alert" class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{form.message}</span>
</div>
{/if}
<label class="fieldset-label" for="email">Email</label>
<input type="email" class="input" placeholder="Email" value="{data.props.email}" />
<input type="text" class="hidden" id="google_id" placeholder="Google ID" value="{data.props.google_id}" />
<input type="email" class="input" placeholder="Email" value={data.props.email} />
<input
type="text"
class="hidden"
id="google_id"
placeholder="Google ID"
value={data.props.google_id}
/>
<label class="fieldset-label" for="first_name">{first_name()}</label>
<input type="text" class="input" id="first_name" placeholder="{first_name()}" value="{data.props.first_name}" />
<input
type="text"
class="input"
id="first_name"
placeholder={first_name()}
value={data.props.first_name}
/>
<label class="fieldset-label" for="last_name">{last_name()}</label>
<input type="text" class="input" id="last_name" placeholder="{last_name()}" value="{data.props.last_name}" />
<label class="fieldset-label" for="allow_admin_access">{allow_family_tree_admin_access()}</label>
<input type="checkbox" class="input" id="allow_admin_access" checked="{data.props.allow_admin_access}" />
<input
type="text"
class="input"
id="last_name"
placeholder={last_name()}
value={data.props.last_name}
/>
<label class="fieldset-label" for="allow_admin_access"
>{allow_family_tree_admin_access()}</label
>
<input
type="checkbox"
class="input"
id="allow_admin_access"
checked={data.props.allow_admin_access}
/>
<label class="fieldset-label" for="birth_date">{born()}</label>
<input type="text" class="input pika-single" id="birth_date" bind:this={birth_date} value={born()} />
<input
type="text"
class="input pika-single"
id="birth_date"
bind:this={birth_date}
value={born()}
/>
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
<input
type="text"
@@ -75,7 +116,7 @@
placeholder={mothers_last_name()}
/>
<label class="fieldset-label" for="mothers_first_name">{mothers_first_name()}</label>
<input
<input
type="text"
class="input"
id="mothers_first_name"

View File

@@ -1,7 +1,7 @@
<script module>
import Page from './+page.svelte';
import Page from './+page.svelte';
</script>
<template>
<Page />
</template>
<Page />
</template>

View File

@@ -1,24 +1,24 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from '@storybook/test';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import { fn } from '@storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
},
},
args: {
onClick: fn(),
}
});
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large']
}
},
args: {
onClick: fn()
}
});
</script>
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->

View File

@@ -1,29 +1,29 @@
<script lang="ts">
import './button.css';
import './button.css';
interface Props {
/** Is this the principal call to action on the page? */
primary?: boolean;
/** What background color to use */
backgroundColor?: string;
/** How large should the button be? */
size?: 'small' | 'medium' | 'large';
/** Button contents */
label: string;
/** The onclick event handler */
onClick?: () => void;
}
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
interface Props {
/** Is this the principal call to action on the page? */
primary?: boolean;
/** What background color to use */
backgroundColor?: string;
/** How large should the button be? */
size?: 'small' | 'medium' | 'large';
/** Button contents */
label: string;
/** The onclick event handler */
onClick?: () => void;
}
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
</script>
<button
type="button"
class={['storybook-button', `storybook-button--${size}`].join(' ')}
class:storybook-button--primary={primary}
class:storybook-button--secondary={!primary}
style:background-color={backgroundColor}
onclick={onClick}
type="button"
class={['storybook-button', `storybook-button--${size}`].join(' ')}
class:storybook-button--primary={primary}
class:storybook-button--secondary={!primary}
style:background-color={backgroundColor}
onclick={onClick}
>
{label}
{label}
</button>

View File

@@ -1,35 +1,37 @@
import { Meta } from "@storybook/blocks";
import { Meta } from '@storybook/blocks';
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
import Github from './assets/github.svg';
import Discord from './assets/discord.svg';
import Youtube from './assets/youtube.svg';
import Tutorials from './assets/tutorials.svg';
import Styling from './assets/styling.png';
import Context from './assets/context.png';
import Assets from './assets/assets.png';
import Docs from './assets/docs.png';
import Share from './assets/share.png';
import FigmaPlugin from './assets/figma-plugin.png';
import Testing from './assets/testing.png';
import Accessibility from './assets/accessibility.png';
import Theming from './assets/theming.png';
import AddonLibrary from './assets/addon-library.png';
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
export const RightArrow = () => (
<svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
);
<Meta title="Configure your project" />
@@ -38,6 +40,7 @@ export const RightArrow = () => <svg
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
@@ -84,6 +87,7 @@ export const RightArrow = () => <svg
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
@@ -203,10 +207,11 @@ export const RightArrow = () => <svg
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>
{`
{`
.sb-container {
margin-bottom: 48px;
}

View File

@@ -1,24 +1,24 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Header from './Header.svelte';
import { fn } from '@storybook/test';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Header from './Header.svelte';
import { fn } from '@storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
}
});
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen'
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn()
}
});
</script>
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />

View File

@@ -1,45 +1,45 @@
<script lang="ts">
import './header.css';
import Button from './Button.svelte';
import './header.css';
import Button from './Button.svelte';
interface Props {
user?: { name: string };
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}
interface Props {
user?: { name: string };
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
</script>
<header>
<div class="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{#if user}
<span class="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
{:else}
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
{/if}
</div>
</div>
<div class="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{#if user}
<span class="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
{:else}
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
{/if}
</div>
</div>
</header>

View File

@@ -1,30 +1,32 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import Page from './Page.svelte';
import { fn } from '@storybook/test';
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import Page from './Page.svelte';
import { fn } from '@storybook/test';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
});
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const { Story } = defineMeta({
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen'
}
});
</script>
<Story name="Logged In" play={async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
<Story
name="Logged In"
play={async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
}}
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
}}
/>
<Story name="Logged Out" />

View File

@@ -1,70 +1,70 @@
<script lang="ts">
import './page.css';
import Header from './Header.svelte';
import './page.css';
import Header from './Header.svelte';
let user = $state<{ name: string }>();
let user = $state<{ name: string }>();
</script>
<article>
<Header
{user}
onLogin={() => (user = { name: 'Jane Doe' })}
onLogout={() => (user = undefined)}
onCreateAccount={() => (user = { name: 'Jane Doe' })}
/>
<Header
{user}
onLogin={() => (user = { name: 'Jane Doe' })}
onLogout={() => (user = undefined)}
onCreateAccount={() => (user = { name: 'Jane Doe' })}
/>
<section class="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a
<a
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
target="_blank"
rel="noopener noreferrer"
>
<strong>component-driven</strong>
</a>
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page data
in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
.
</p>
<div class="tip-wrapper">
<span class="tip">Tip</span>
Adjust the width of the canvas with the
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
<section class="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a
<a
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
target="_blank"
rel="noopener noreferrer"
>
<strong>component-driven</strong>
</a>
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page data
in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
.
</p>
<div class="tip-wrapper">
<span class="tip">Tip</span>
Adjust the width of the canvas with the
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0
010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>

View File

@@ -1,30 +1,30 @@
.storybook-button {
display: inline-block;
cursor: pointer;
border: 0;
border-radius: 3em;
font-weight: 700;
line-height: 1;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
display: inline-block;
cursor: pointer;
border: 0;
border-radius: 3em;
font-weight: 700;
line-height: 1;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.storybook-button--primary {
background-color: #555ab9;
color: white;
background-color: #555ab9;
color: white;
}
.storybook-button--secondary {
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
background-color: transparent;
color: #333;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
background-color: transparent;
color: #333;
}
.storybook-button--small {
padding: 10px 16px;
font-size: 12px;
padding: 10px 16px;
font-size: 12px;
}
.storybook-button--medium {
padding: 11px 20px;
font-size: 14px;
padding: 11px 20px;
font-size: 14px;
}
.storybook-button--large {
padding: 12px 24px;
font-size: 16px;
padding: 12px 24px;
font-size: 16px;
}

View File

@@ -1,32 +1,32 @@
.storybook-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.storybook-header svg {
display: inline-block;
vertical-align: top;
display: inline-block;
vertical-align: top;
}
.storybook-header h1 {
display: inline-block;
vertical-align: top;
margin: 6px 0 6px 10px;
font-weight: 700;
font-size: 20px;
line-height: 1;
display: inline-block;
vertical-align: top;
margin: 6px 0 6px 10px;
font-weight: 700;
font-size: 20px;
line-height: 1;
}
.storybook-header button + button {
margin-left: 10px;
margin-left: 10px;
}
.storybook-header .welcome {
margin-right: 10px;
color: #333;
font-size: 14px;
margin-right: 10px;
color: #333;
font-size: 14px;
}

View File

@@ -1,68 +1,68 @@
.storybook-page {
margin: 0 auto;
padding: 48px 20px;
max-width: 600px;
color: #333;
font-size: 14px;
line-height: 24px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0 auto;
padding: 48px 20px;
max-width: 600px;
color: #333;
font-size: 14px;
line-height: 24px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.storybook-page h2 {
display: inline-block;
vertical-align: top;
margin: 0 0 4px;
font-weight: 700;
font-size: 32px;
line-height: 1;
display: inline-block;
vertical-align: top;
margin: 0 0 4px;
font-weight: 700;
font-size: 32px;
line-height: 1;
}
.storybook-page p {
margin: 1em 0;
margin: 1em 0;
}
.storybook-page a {
color: inherit;
color: inherit;
}
.storybook-page ul {
margin: 1em 0;
padding-left: 30px;
margin: 1em 0;
padding-left: 30px;
}
.storybook-page li {
margin-bottom: 8px;
margin-bottom: 8px;
}
.storybook-page .tip {
display: inline-block;
vertical-align: top;
margin-right: 10px;
border-radius: 1em;
background: #e7fdd8;
padding: 4px 12px;
color: #357a14;
font-weight: 700;
font-size: 11px;
line-height: 12px;
display: inline-block;
vertical-align: top;
margin-right: 10px;
border-radius: 1em;
background: #e7fdd8;
padding: 4px 12px;
color: #357a14;
font-weight: 700;
font-size: 11px;
line-height: 12px;
}
.storybook-page .tip-wrapper {
margin-top: 40px;
margin-bottom: 40px;
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
font-size: 13px;
line-height: 20px;
}
.storybook-page .tip-wrapper svg {
display: inline-block;
vertical-align: top;
margin-top: 3px;
margin-right: 4px;
width: 12px;
height: 12px;
display: inline-block;
vertical-align: top;
margin-top: 3px;
margin-right: 4px;
width: 12px;
height: 12px;
}
.storybook-page .tip-wrapper svg path {
fill: #1ea7fd;
fill: #1ea7fd;
}