| @@ -173,8 +173,8 @@ class PostsController < ApplicationController | |||
| return head :unauthorized unless current_user | |||
| return head :forbidden unless current_user.gte_member? | |||
| force = truthy_param?(params[:force]) | |||
| merge = truthy_param?(params[:merge]) | |||
| force = bool?(:force) | |||
| merge = bool?(:merge) | |||
| return head :bad_request if force && merge | |||
| base_version_no = nil | |||
| @@ -201,15 +201,14 @@ class PostsController < ApplicationController | |||
| base_snapshot = post_snapshot_from_version(base_version) | |||
| current_snapshot = post_snapshot_from_record(post) | |||
| end | |||
| incoming_snapshot = post_incoming_snapshot(post, | |||
| title:, | |||
| incoming_snapshot = post_incoming_snapshot(title:, | |||
| original_created_from:, | |||
| original_created_before:, | |||
| tag_names:, | |||
| parent_post_ids:) | |||
| snapshot_to_apply = | |||
| if post.version_no == base_version_no || force | |||
| if force || post.version_no == base_version_no || current_snapshot == base_snapshot | |||
| incoming_snapshot | |||
| else | |||
| changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot) | |||
| @@ -448,30 +447,36 @@ class PostsController < ApplicationController | |||
| version_no | |||
| end | |||
| def truthy_param?(value) = ActiveModel::Type::Boolean.new.cast(value) | |||
| def post_snapshot_from_version version | |||
| { title: version.title, | |||
| original_created_from: snapshot_time(version.original_created_from), | |||
| original_created_before: snapshot_time(version.original_created_before), | |||
| tag_names: version.tags.to_s.split.filter { !(_1.start_with?('nico:')) }.sort, | |||
| tag_names: editable_tag_names_from_version(version), | |||
| parent_post_ids: snapshot_parent_post_ids_from_version(version) } | |||
| end | |||
| def editable_tag_names_from_version version | |||
| version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort | |||
| end | |||
| def post_snapshot_from_record post | |||
| { title: post.title, | |||
| original_created_from: snapshot_time(post.original_created_from), | |||
| original_created_before: snapshot_time(post.original_created_before), | |||
| tag_names: post.tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name'), | |||
| tag_names: editable_tag_names_from_post(post), | |||
| parent_post_ids: post.parent_posts.order(:id).pluck(:id) } | |||
| end | |||
| def post_incoming_snapshot post, title:, original_created_from:, original_created_before:, | |||
| tag_names:, parent_post_ids: | |||
| def editable_tag_names_from_post post | |||
| post.tags.not_nico.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') | |||
| end | |||
| def post_incoming_snapshot title:, original_created_from:, original_created_before:, | |||
| tag_names:, parent_post_ids: | |||
| { title:, | |||
| original_created_from: snapshot_time(original_created_from), | |||
| original_created_before: snapshot_time(original_created_before), | |||
| tag_names: incoming_tag_names_for_snapshot(post, tag_names), | |||
| tag_names: incoming_tag_names_for_snapshot(tag_names), | |||
| parent_post_ids: parent_post_ids.sort } | |||
| end | |||
| @@ -494,44 +499,10 @@ class PostsController < ApplicationController | |||
| value.to_s | |||
| end | |||
| def incoming_tag_names_for_snapshot post, raw_tag_names | |||
| manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names) | |||
| existing_tags = | |||
| Tag | |||
| .joins(:tag_name) | |||
| .where(tag_names: { name: manual_names }) | |||
| .to_a | |||
| def incoming_tag_names_for_snapshot raw_tag_names | |||
| tags = Tag.normalise_tags!(raw_tag_names, with_tagme: false) | |||
| expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name) | |||
| (manual_names + expanded_names).uniq.sort | |||
| end | |||
| def normalised_manual_tag_names_for_snapshot raw_tag_names | |||
| if raw_tag_names.any? { |name| name.downcase.start_with?('nico:') } | |||
| raise Tag::NicoTagNormalisationError | |||
| end | |||
| pairs = raw_tag_names.map do |raw_name| | |||
| prefix, category = | |||
| Tag::CATEGORY_PREFIXES.find { |p, _| raw_name.downcase.start_with?(p) } || ['', nil] | |||
| name = TagName.canonicalise(raw_name.sub(/\A#{ Regexp.escape(prefix) }/i, '')).first | |||
| [name, category] | |||
| end | |||
| names = pairs.map(&:first) | |||
| has_deerjikist = pairs.any? do |name, category| | |||
| category == :deerjikist || | |||
| Tag.joins(:tag_name).where(category: :deerjikist, tag_names: { name: }).exists? | |||
| end | |||
| names << Tag.no_deerjikist.name unless has_deerjikist | |||
| names.uniq.sort | |||
| Tag.expand_parent_tags(tags).map(&:name).uniq.sort | |||
| end | |||
| def post_conflict_json post:, base_version_no:, base_snapshot:, | |||
| @@ -618,29 +589,53 @@ class PostsController < ApplicationController | |||
| original_created_from: snapshot[:original_created_from], | |||
| original_created_before: snapshot[:original_created_before]) | |||
| tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false) | |||
| TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user) | |||
| editable_tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false) | |||
| TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user) | |||
| readonly_tags = post.tags.nico.to_a | |||
| tags = readonly_tags + editable_tags | |||
| tags = Tag.expand_parent_tags(tags) | |||
| sync_post_tags!(post, tags) | |||
| sync_post_tags!(post, tags) | |||
| sync_parent_posts!(post, snapshot[:parent_post_ids]) | |||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | |||
| end | |||
| def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot | |||
| [:title, :original_created_from, :original_created_before, :tag_names, :parent_post_ids].map { | |||
| [_1, merge_scaler_snapshot_value(base_snapshot[_1], | |||
| [:title, :original_created_from, :original_created_before].map { | |||
| [_1, merge_scalar_snapshot_value(base_snapshot[_1], | |||
| current_snapshot[_1], | |||
| incoming_snapshot[_1])] | |||
| }.to_h | |||
| }.to_h.merge([:tag_names, :parent_post_ids].map { | |||
| [_1, merge_set_snapshot_value(base_snapshot[_1], | |||
| current_snapshot[_1], | |||
| incoming_snapshot[_1])] | |||
| }.to_h) | |||
| end | |||
| def merge_scaler_snapshot_value base, current, mine | |||
| def merge_scalar_snapshot_value base, current, mine | |||
| return mine if current == base | |||
| return current if mine == base || current == mine | |||
| raise ArgumentError, '競合してゐる項目はマージできません.' | |||
| end | |||
| def merge_set_snapshot_value base, current, mine | |||
| base = base.to_a | |||
| current = current.to_a | |||
| mine = mine.to_a | |||
| added_by_current = current - base | |||
| removed_by_current = base - current | |||
| added_by_me = mine - base | |||
| removed_by_me = base - mine | |||
| merged = base + added_by_current + added_by_me | |||
| merged -= removed_by_current | |||
| merged -= removed_by_me | |||
| merged.uniq.sort | |||
| end | |||
| end | |||
| @@ -28,6 +28,8 @@ class Post < ApplicationRecord | |||
| has_one_attached :thumbnail | |||
| attribute :version_no, :integer, default: 1 | |||
| before_validation :normalise_url | |||
| validates :url, presence: true, uniqueness: true | |||
| @@ -40,6 +40,8 @@ class Tag < ApplicationRecord | |||
| belongs_to :tag_name | |||
| delegate :wiki_page, to: :tag_name | |||
| attribute :version_no, :integer, default: 1 | |||
| delegate :name, to: :tag_name, allow_nil: true | |||
| validates :tag_name, presence: true | |||
| @@ -136,7 +138,6 @@ class Tag < ApplicationRecord | |||
| tn = tn.canonical if tn.canonical_id? | |||
| Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t| | |||
| t.version_no = TagVersion.where(tag_id: t.id).order(version_no: :desc).first || 1 | |||
| t.category = category | |||
| end | |||
| rescue ActiveRecord::RecordNotUnique | |||
| @@ -15,6 +15,8 @@ class WikiPage < ApplicationRecord | |||
| has_many :wiki_versions | |||
| attribute :version_no, :integer, default: 1 | |||
| belongs_to :tag_name | |||
| validates :tag_name, presence: true | |||
| validates :body, presence: true | |||
| @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { updatePost } from '@/lib/posts' | |||
| import type { FC } from 'react' | |||
| import type { FC, FormEvent } from 'react' | |||
| import type { Post, Tag } from '@/types' | |||
| @@ -33,6 +33,7 @@ type Props = { post: Post | |||
| export default (({ post, onSave }: Props) => { | |||
| const [disabled, setDisabled] = useState (false) | |||
| const [originalCreatedBefore, setOriginalCreatedBefore] = | |||
| useState<string | null> (post.originalCreatedBefore) | |||
| const [originalCreatedFrom, setOriginalCreatedFrom] = | |||
| @@ -61,7 +62,9 @@ export default (({ post, onSave }: Props) => { | |||
| } | |||
| catch (e) | |||
| { | |||
| if (e.response.status !== 409) | |||
| const response = (e as any)?.response | |||
| if (response?.status !== 409) | |||
| { | |||
| toast ({ description: '更新はできなかったよ……' }) | |||
| return | |||
| @@ -74,7 +77,7 @@ export default (({ post, onSave }: Props) => { | |||
| <p>ほかの耕作員が先に更新してゐます.</p> | |||
| <p>現在の変更をどう扱ひますか?</p> | |||
| </div>), | |||
| choices: [{ value: 'merge', label: '差分をマージ' }, | |||
| choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []), | |||
| { value: 'overwrite', label: '強制上書き', variant: 'danger' }] }) | |||
| if (action === 'merge') | |||
| @@ -96,12 +99,14 @@ export default (({ post, onSave }: Props) => { | |||
| } | |||
| } | |||
| const handleSubmit = async e => { | |||
| const handleSubmit = async (e: FormEvent) => { | |||
| e.preventDefault () | |||
| setDisabled (true) | |||
| await update ({ id: post.id, title, tags, parentPostIds, | |||
| originalCreatedFrom, originalCreatedBefore }, | |||
| { baseVersionNo: post.versionNo }) | |||
| setDisabled (false) | |||
| } | |||
| useEffect (() => { | |||
| @@ -113,10 +118,12 @@ export default (({ post, onSave }: Props) => { | |||
| {/* タイトル */} | |||
| <div> | |||
| <Label>タイトル</Label> | |||
| <input type="text" | |||
| className="w-full border rounded p-2" | |||
| value={title ?? ''} | |||
| onChange={ev => setTitle (ev.target.value)}/> | |||
| <input | |||
| type="text" | |||
| disabled={disabled} | |||
| className="w-full border rounded p-2" | |||
| value={title ?? ''} | |||
| onChange={ev => setTitle (ev.target.value)}/> | |||
| </div> | |||
| {/* 親投稿 */} | |||
| @@ -124,24 +131,32 @@ export default (({ post, onSave }: Props) => { | |||
| <Label>親投稿</Label> | |||
| <input | |||
| type="text" | |||
| disabled={disabled} | |||
| value={parentPostIds} | |||
| onChange={e => setParentPostIds (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* タグ */} | |||
| <PostFormTagsArea tags={tags} setTags={setTags}/> | |||
| <PostFormTagsArea | |||
| disabled={disabled} | |||
| tags={tags} | |||
| setTags={setTags}/> | |||
| {/* オリジナルの作成日時 */} | |||
| <PostOriginalCreatedTimeField | |||
| disabled={disabled} | |||
| originalCreatedFrom={originalCreatedFrom} | |||
| setOriginalCreatedFrom={setOriginalCreatedFrom} | |||
| originalCreatedBefore={originalCreatedBefore} | |||
| setOriginalCreatedBefore={setOriginalCreatedBefore}/> | |||
| {/* 送信 */} | |||
| <Button onClick={handleSubmit} | |||
| className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||
| <Button | |||
| type="submit" | |||
| disabled={disabled} | |||
| onClick={handleSubmit} | |||
| className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||
| 更新 | |||
| </Button> | |||
| </div>) | |||
| @@ -7,7 +7,7 @@ import Label from '@/components/common/Label' | |||
| import TextArea from '@/components/common/TextArea' | |||
| import { apiGet } from '@/lib/api' | |||
| import type { FC, SyntheticEvent } from 'react' | |||
| import type { ComponentPropsWithoutRef, FC, SyntheticEvent } from 'react' | |||
| import type { Tag } from '@/types' | |||
| @@ -31,12 +31,13 @@ const replaceToken = (value: string, start: number, end: number, text: string) = | |||
| `${ value.slice (0, start) }${ text }${ value.slice (end) }` | |||
| type Props = { | |||
| tags: string | |||
| setTags: (tags: string) => void } | |||
| type Props = | |||
| & { tags: string | |||
| setTags: (tags: string) => void } | |||
| & ComponentPropsWithoutRef<'textarea'> | |||
| export default (({ tags, setTags }: Props) => { | |||
| export default (({ tags, setTags, ...rest }: Props) => { | |||
| const ref = useRef<HTMLTextAreaElement> (null) | |||
| const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 }) | |||
| @@ -87,7 +88,8 @@ export default (({ tags, setTags }: Props) => { | |||
| onBlur={() => { | |||
| setFocused (false) | |||
| setSuggestionsVsbl (false) | |||
| }}/> | |||
| }} | |||
| {...rest}/> | |||
| {focused && ( | |||
| <TagSearchBox | |||
| suggestions={suggestionsVsbl && suggestions.length > 0 | |||
| @@ -5,13 +5,15 @@ import { Button } from '@/components/ui/button' | |||
| import type { FC } from 'react' | |||
| type Props = { | |||
| disabled?: boolean | |||
| originalCreatedFrom: string | null | |||
| setOriginalCreatedFrom: (x: string | null) => void | |||
| originalCreatedBefore: string | null | |||
| setOriginalCreatedBefore: (x: string | null) => void } | |||
| export default (({ originalCreatedFrom, | |||
| export default (({ disabled, | |||
| originalCreatedFrom, | |||
| setOriginalCreatedFrom, | |||
| originalCreatedBefore, | |||
| setOriginalCreatedBefore }: Props) => ( | |||
| @@ -21,6 +23,7 @@ export default (({ originalCreatedFrom, | |||
| <div className="w-80"> | |||
| <DateTimeField | |||
| className="mr-2" | |||
| disabled={disabled ?? false} | |||
| value={originalCreatedFrom ?? undefined} | |||
| onChange={setOriginalCreatedFrom} | |||
| onBlur={ev => { | |||
| @@ -40,6 +43,7 @@ export default (({ originalCreatedFrom, | |||
| <div> | |||
| <Button | |||
| className="bg-gray-600 text-white rounded" | |||
| disabled={disabled} | |||
| onClick={() => { | |||
| setOriginalCreatedFrom (null) | |||
| }}> | |||
| @@ -51,6 +55,7 @@ export default (({ originalCreatedFrom, | |||
| <div className="w-80"> | |||
| <DateTimeField | |||
| className="mr-2" | |||
| disabled={disabled} | |||
| value={originalCreatedBefore ?? undefined} | |||
| onChange={setOriginalCreatedBefore}/> | |||
| より前 | |||
| @@ -58,6 +63,7 @@ export default (({ originalCreatedFrom, | |||
| <div> | |||
| <Button | |||
| className="bg-gray-600 text-white rounded" | |||
| disabled={disabled} | |||
| onClick={() => { | |||
| setOriginalCreatedBefore (null) | |||
| }}> | |||
| @@ -36,12 +36,12 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { | |||
| { name: '一覧', to: '/posts' }, | |||
| { name: '検索', to: '/posts/search' }, | |||
| { name: '追加', to: '/posts/new' }, | |||
| { name: '履歴', to: '/posts/changes' }, | |||
| { name: '全体履歴', to: '/posts/changes' }, | |||
| { name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] }, | |||
| { name: 'タグ', to: '/tags', subMenu: [ | |||
| { name: 'マスタ', to: '/tags' }, | |||
| { name: 'ニコニコ連携', to: '/tags/nico' }, | |||
| { name: '履歴', to: '/tags/changes' }, | |||
| { name: '全体履歴', to: '/tags/changes' }, | |||
| { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }, | |||
| { component: <Separator/>, visible: tagFlg }, | |||
| { name: `広場 (${ postCount || 0 })`, | |||
| @@ -53,7 +53,7 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { | |||
| { name: '一覧', to: '/materials' }, | |||
| { name: '検索', to: '/materials/search', visible: false }, | |||
| { name: '追加', to: '/materials/new' }, | |||
| { name: '履歴', to: '/materials/changes', visible: false }, | |||
| { name: '全体履歴', to: '/materials/changes', visible: false }, | |||
| { name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] }, | |||
| { name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [ | |||
| { name: <>第 1 会場</>, to: '/theatres/1' }, | |||
| @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' | |||
| import { cn } from '@/lib/utils' | |||
| import type { FC, FocusEvent } from 'react' | |||
| import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react' | |||
| const pad = (n: number): string => n.toString ().padStart (2, '0') | |||
| @@ -18,14 +18,14 @@ const toDateTimeLocalValue = (d: Date) => { | |||
| } | |||
| type Props = { | |||
| type Props = Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> & { | |||
| value?: string | |||
| onChange?: (isoUTC: string | null) => void | |||
| className?: string | |||
| onBlur?: (ev: FocusEvent<HTMLInputElement>) => void } | |||
| export default (({ value, onChange, className, onBlur }: Props) => { | |||
| export default (({ value, onChange, className, onBlur, ...rest }: Props) => { | |||
| const [local, setLocal] = useState ('') | |||
| useEffect (() => { | |||
| @@ -42,5 +42,6 @@ export default (({ value, onChange, className, onBlur }: Props) => { | |||
| setLocal (v) | |||
| onChange?.(v ? (new Date (v)).toISOString () : null) | |||
| }} | |||
| onBlur={onBlur}/>) | |||
| onBlur={onBlur} | |||
| {...rest}/>) | |||
| }) satisfies FC<Props> | |||
| @@ -37,25 +37,27 @@ const DialogContent = React.forwardRef< | |||
| <DialogOverlay /> | |||
| <DialogPrimitive.Content | |||
| ref={ref} | |||
| className={cn( | |||
| 'fixed left-[50%] top-[50%] z-50 w-[90%] grid max-w-lg', | |||
| className={cn ( | |||
| 'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg', | |||
| 'translate-x-[-50%] translate-y-[-50%]', | |||
| 'gap-4 border bg-gray-300/80 dark:bg-gray-700/80', | |||
| 'p-6 shadow-lg duration-200', | |||
| 'gap-5 rounded-2xl border border-border', | |||
| 'bg-background p-6 text-foreground shadow-2xl', | |||
| 'duration-200', | |||
| 'data-[state=open]:animate-in data-[state=closed]:animate-out', | |||
| 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', | |||
| 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', | |||
| 'data-[state=closed]:slide-out-to-left-1/2', | |||
| 'data-[state=closed]:slide-out-to-top-[48%]', | |||
| 'data-[state=open]:slide-in-from-left-1/2', | |||
| 'data-[state=open]:slide-in-from-top-[48%] rounded-lg', | |||
| className)} | |||
| {...props} | |||
| > | |||
| {children} | |||
| <DialogPrimitive.Close className="absolute right-4 top-4 bg-red-500 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | |||
| <X className="h-3 w-3" /> | |||
| <span className="sr-only">Close</span> | |||
| <DialogPrimitive.Close | |||
| className={cn ( | |||
| 'absolute right-4 top-4 rounded-full p-1', | |||
| 'text-muted-foreground opacity-70 transition-opacity', | |||
| 'hover:bg-accent hover:text-accent-foreground hover:opacity-100', | |||
| 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2')}> | |||
| <X className="h-4 w-4"/> | |||
| <span className="sr-only">Close</span> | |||
| </DialogPrimitive.Close> | |||
| </DialogPrimitive.Content> | |||
| </DialogPortal> | |||
| @@ -6,6 +6,56 @@ | |||
| @layer base | |||
| { | |||
| :root | |||
| { | |||
| --background: 0 0% 100%; | |||
| --foreground: 222.2 84% 4.9%; | |||
| --primary: 222.2 47.4% 11.2%; | |||
| --primary-foreground: 210 40% 98%; | |||
| --secondary: 210 40% 96.1%; | |||
| --secondary-foreground: 222.2 47.4% 11.2%; | |||
| --destructive: 0 72.2% 50.6%; | |||
| --destructive-foreground: 210 40% 98%; | |||
| --muted: 210 40% 96.1%; | |||
| --muted-foreground: 215.4 16.3% 46.9%; | |||
| --accent: 210 40% 96.1%; | |||
| --accent-foreground: 222.2 47.4% 11.2%; | |||
| --border: 214.3 31.8% 91.4%; | |||
| --input: 214.3 31.8% 91.4%; | |||
| --ring: 222.2 84% 4.9%; | |||
| } | |||
| .dark | |||
| { | |||
| --background: 222.2 84% 4.9%; | |||
| --foreground: 210 40% 98%; | |||
| --primary: 210 40% 98%; | |||
| --primary-foreground: 222.2 47.4% 11.2%; | |||
| --secondary: 217.2 32.6% 17.5%; | |||
| --secondary-foreground: 210 40% 98%; | |||
| --destructive: 0 62.8% 45%; | |||
| --destructive-foreground: 210 40% 98%; | |||
| --muted: 217.2 32.6% 17.5%; | |||
| --muted-foreground: 215 20.2% 65.1%; | |||
| --accent: 217.2 32.6% 17.5%; | |||
| --accent-foreground: 210 40% 98%; | |||
| --border: 217.2 32.6% 17.5%; | |||
| --input: 217.2 32.6% 17.5%; | |||
| --ring: 212.7 26.8% 83.9%; | |||
| } | |||
| body | |||
| { | |||
| @apply overflow-x-clip; | |||
| @@ -54,34 +104,6 @@ body | |||
| min-height: 100dvh; | |||
| } | |||
| h1 | |||
| { | |||
| font-size: 3.2em; | |||
| line-height: 1.1; | |||
| } | |||
| button | |||
| { | |||
| border-radius: 8px; | |||
| border: 1px solid transparent; | |||
| padding: 0.6em 1.2em; | |||
| font-size: 1em; | |||
| font-weight: 500; | |||
| font-family: inherit; | |||
| background-color: #1a1a1a; | |||
| cursor: pointer; | |||
| transition: border-color 0.25s; | |||
| } | |||
| button:hover | |||
| { | |||
| border-color: #646cff; | |||
| } | |||
| button:focus, | |||
| button:focus-visible | |||
| { | |||
| outline: 4px auto -webkit-focus-ring-color; | |||
| } | |||
| @media (prefers-color-scheme: light) | |||
| { | |||
| :root | |||
| @@ -19,7 +19,22 @@ export default { | |||
| 'rainbow-scroll': 'rainbow-scroll .25s linear infinite' }, | |||
| colors: { | |||
| red: { 925: '#5f1414', | |||
| 975: '#230505' } }, | |||
| 975: '#230505' }, | |||
| border: 'hsl(var(--border))', | |||
| input: 'hsl(var(--input))', | |||
| ring: 'hsl(var(--ring))', | |||
| background: 'hsl(var(--background))', | |||
| foreground: 'hsl(var(--foreground))', | |||
| primary: { DEFAULT: 'hsl(var(--primary))', | |||
| foreground: 'hsl(var(--primary-foreground))' }, | |||
| secondary: { DEFAULT: 'hsl(var(--secondary))', | |||
| foreground: 'hsl(var(--secondary-foreground))' }, | |||
| destructive: { DEFAULT: 'hsl(var(--destructive))', | |||
| foreground: 'hsl(var(--destructive-foreground))' }, | |||
| muted: { DEFAULT: 'hsl(var(--muted))', | |||
| foreground: 'hsl(var(--muted-foreground))' }, | |||
| accent: { DEFAULT: 'hsl(var(--accent))', | |||
| foreground: 'hsl(var(--accent-foreground))' } }, | |||
| keyframes: { | |||
| 'rainbow-scroll': { | |||
| '0%': { backgroundPosition: '0% 50%' }, | |||