Merge branch 'claude/design-system-setup-46d8aa' into hub
디자인 시스템 기반을 Tailwind에서 @fox(SCSS + CSS Modules)로 교체한다. ⚠️ 기존 화면(꾸미기 아이템 포함)의 Tailwind 클래스는 이 시점부터 무효다 — 라우팅·로직은 정상이나 시각 스타일이 빠진 상태이며, 화면을 @fox 컴포넌트로 재작성하며 복구한다. components/ui/의 기존 22개는 소비처가 남아 있어 함께 교체될 때까지 유지한다. Co-Authored-By: Claude Opus 5
@13bf742269bfb3344496b4143863f5067a45cfdc
+++ @fox/README.md
... | ... | @@ -0,0 +1,123 @@ |
| 1 | +# @fox — 포터블 디자인 시스템 | |
| 2 | + | |
| 3 | +프로젝트에 종속되지 않는 디자인 시스템 패키지. **이 폴더를 통째로 복사하고 설정 2줄을 | |
| 4 | +추가하면** 다른 프로젝트에서 그대로 동작합니다. | |
| 5 | + | |
| 6 | +``` | |
| 7 | +@fox/ | |
| 8 | + styles/ SCSS 토큰 · 함수 · 믹스인 | |
| 9 | + index.scss 글로벌 진입점 (앱이 딱 한 번 import — CSS 출력) | |
| 10 | + abstracts.scss 저작 진입점 (컴포넌트가 @use — CSS 출력 0) | |
| 11 | + tokens/ 토큰 map = 단일 진실 공급원(SSOT) | |
| 12 | + _root.scss map → --fox-* 커스텀 프로퍼티 생성 | |
| 13 | + _reset.scss 리셋 & 베이스 | |
| 14 | + _functions.scss fox.color() 등 토큰 접근 + 검증 | |
| 15 | + _mixins.scss fox.typo() · fox.media() 등 조합 패턴 | |
| 16 | + core/ | |
| 17 | + components/ 공용 컴포넌트 (규약은 components/README.md) | |
| 18 | + utils/ cx() 등 의존성 없는 유틸 | |
| 19 | +``` | |
| 20 | + | |
| 21 | +## 다른 프로젝트로 이식하기 | |
| 22 | + | |
| 23 | +1. `@fox/` 폴더를 대상 프로젝트 루트에 복사합니다. | |
| 24 | + | |
| 25 | +2. `sass`를 설치합니다. | |
| 26 | + | |
| 27 | + ```bash | |
| 28 | + npm install --save-dev sass | |
| 29 | + ``` | |
| 30 | + | |
| 31 | +3. **TypeScript 경로 별칭** — `tsconfig.json`: | |
| 32 | + | |
| 33 | + ```json | |
| 34 | + { "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } } | |
| 35 | + ``` | |
| 36 | + | |
| 37 | +4. **SCSS 로드 경로** — `next.config.ts`: | |
| 38 | + | |
| 39 | + ```ts | |
| 40 | + sassOptions: { loadPaths: [path.join(process.cwd())] } | |
| 41 | + ``` | |
| 42 | + | |
| 43 | + TypeScript의 `paths`는 번들러 전용이라 Sass의 `@use` 해석에는 관여하지 않습니다. | |
| 44 | + 그래서 같은 경로를 양쪽에 각각 선언해야 TS와 SCSS의 import 표기가 일치합니다. | |
| 45 | + 이게 없으면 컴포넌트마다 `../../../` 상대 경로를 써야 해서 폴더 복사가 불가능해집니다. | |
| 46 | + | |
| 47 | +5. 앱의 글로벌 스타일에서 진입점을 한 번 import 합니다. | |
| 48 | + | |
| 49 | + ```scss | |
| 50 | + /* app/globals.scss */ | |
| 51 | + @use "@fox/styles"; | |
| 52 | + ``` | |
| 53 | + | |
| 54 | +6. (선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고. | |
| 55 | + | |
| 56 | +Next.js가 아닌 번들러라면 4번만 그 도구의 Sass 옵션(`includePaths` / `loadPaths`)으로 | |
| 57 | +바꿔주면 됩니다. | |
| 58 | + | |
| 59 | +## 핵심 설계 | |
| 60 | + | |
| 61 | +**토큰의 SSOT는 `tokens/`의 SCSS map 하나뿐입니다.** 여기서 두 가지가 함께 파생됩니다 — | |
| 62 | +`_root.scss`가 만드는 `--fox-*` CSS 커스텀 프로퍼티와, `_functions.scss`가 검증에 쓰는 | |
| 63 | +유효 이름 목록. 값과 이름이 한 곳에만 있으므로 둘이 어긋날 수 없습니다. | |
| 64 | + | |
| 65 | +**규율을 컴파일러가 강제합니다.** 화면 코드는 `color: #4f46e5`를 쓸 수 없고 | |
| 66 | +`fox.color(primary)`로 토큰을 지목해야 합니다. 없는 이름을 쓰면 빌드가 실패하며, 에러 | |
| 67 | +메시지가 사용 가능한 토큰 목록을 함께 출력합니다. | |
| 68 | + | |
| 69 | +``` | |
| 70 | +Error: [@fox] 알 수 없는 color 토큰: `primry` — 사용 가능한 값: primary, primary-hover, ... | |
| 71 | +``` | |
| 72 | + | |
| 73 | +**1rem = 10px입니다.** `_reset.scss`의 `html { font-size: 62.5% }`가 근거입니다. px → rem | |
| 74 | +환산이 10으로 나누기가 되어 토큰 값이 읽기 쉽고, px 고정과 달리 사용자가 브라우저 글자 | |
| 75 | +크기를 키우면 앱 전체가 비례해 확대됩니다. | |
| 76 | + | |
| 77 | +> ⚠️ **미디어 쿼리 안의 `rem`은 예외입니다.** 미디어 쿼리는 `html`의 `font-size` 영향을 | |
| 78 | +> 받지 않고 항상 브라우저 기본 크기(보통 16px) 기준으로 계산합니다. 그래서 | |
| 79 | +> `tokens/_layout.scss`의 브레이크포인트 `48rem`은 480px가 아니라 **768px**입니다. | |
| 80 | +> 각 값에 px 환산을 주석으로 달아 두었습니다. | |
| 81 | + | |
| 82 | +**라이트/다크는 값이 한 곳에만 존재합니다.** `light-dark(라이트, 다크)`로 한 줄에 두 값을 | |
| 83 | +함께 쓰고, 전환은 `color-scheme`이 담당합니다. 다크 전용 오버라이드 블록이 없으므로 한쪽만 | |
| 84 | +고쳐서 생기는 테마 불일치가 구조적으로 불가능합니다. | |
| 85 | +`<html data-theme="light"|"dark">`로 수동 선택을 덮어쓸 수 있고, 속성이 없으면 OS 설정을 | |
| 86 | +따릅니다. | |
| 87 | + | |
| 88 | +> **브라우저 지원** — `light-dark()` 자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이 | |
| 89 | +> 필요하지만, Next.js 16(Turbopack)의 Lightning CSS가 현재 설정에서 이를 | |
| 90 | +> `var(--lightningcss-light, 라이트) var(--lightningcss-dark, 다크)` 조합으로 폴리필해 | |
| 91 | +> 출력하므로 구형 브라우저에서도 동작합니다. browserslist 타깃을 좁히면 폴리필이 빠지고 | |
| 92 | +> 원본 `light-dark()`가 그대로 나가므로, 타깃을 바꿀 때 이 전제를 다시 확인하세요. | |
| 93 | +> Lightning CSS를 쓰지 않는 번들러로 옮길 때도 마찬가지입니다. | |
| 94 | + | |
| 95 | +## 호스트 앱과의 계약 | |
| 96 | + | |
| 97 | +`@fox`가 소유하지 않고 앱에서 받는 값입니다. | |
| 98 | + | |
| 99 | +| CSS 변수 | 용도 | 미주입 시 | | |
| 100 | +| --- | --- | --- | | |
| 101 | +| `--app-font-sans` | 기본 서체 | `system-ui`로 폴백 | | |
| 102 | +| `--app-font-mono` | 고정폭 서체 | `ui-monospace`로 폴백 | | |
| 103 | + | |
| 104 | +폰트 파일은 앱이 소유합니다(Next.js에서는 `next/font`가 최적화·자체 호스팅을 담당). `@fox`는 | |
| 105 | +변수를 참조만 하므로, 폴더를 복사해 간 프로젝트는 그 프로젝트의 폰트를 그대로 씁니다. | |
| 106 | + | |
| 107 | +```tsx | |
| 108 | +// app/layout.tsx | |
| 109 | +const sans = localFont({ src: [...], variable: "--app-font-sans" }); | |
| 110 | +<html className={sans.variable}> | |
| 111 | +``` | |
| 112 | + | |
| 113 | +테마 토글을 붙이려면 `<html>`의 `data-theme` 속성을 `"light" | "dark"`로 설정하고, 전환 | |
| 114 | +애니메이션이 필요하면 같은 tick에 `data-theme-transition` 속성을 부여한 뒤 200ms 후 | |
| 115 | +제거합니다(`_root.scss`가 그 동안만 색상 트랜지션을 겁니다). | |
| 116 | + | |
| 117 | +## 토큰 추가하기 | |
| 118 | + | |
| 119 | +1. `tokens/`의 해당 map에 항목을 추가합니다. 그것으로 끝입니다 — 커스텀 프로퍼티 생성과 | |
| 120 | + 함수 검증 목록이 자동으로 따라옵니다. | |
| 121 | +2. 값의 출처(Figma file/node 등)를 주석으로 남깁니다. | |
| 122 | +3. **새 토큰 그룹**(map 자체를 신설)이 필요하면 `tokens/_index.scss`에 `@forward`를 추가하고, | |
| 123 | + `_functions.scss`에 접근 함수를, `_root.scss`에 `@each` 블록을 각각 추가합니다. |
+++ @fox/core/components/README.md
... | ... | @@ -0,0 +1,87 @@ |
| 1 | +# 컴포넌트 작성 규약 | |
| 2 | + | |
| 3 | +아직 컴포넌트가 없습니다. 디자인 확정에 따라 하나씩 추가합니다. | |
| 4 | + | |
| 5 | +## 파일 배치 | |
| 6 | + | |
| 7 | +컴포넌트 하나당 폴더 하나. 스타일을 같은 폴더에 두어야 폴더째 복사·삭제가 가능합니다. | |
| 8 | + | |
| 9 | +``` | |
| 10 | +components/ | |
| 11 | + button/ | |
| 12 | + button.tsx 컴포넌트 | |
| 13 | + button.module.scss 스타일 (반드시 .module.scss — 전역 오염 방지) | |
| 14 | + index.ts export { Button } from "./button"; | |
| 15 | + index.ts 배럴 — export * from "./button"; | |
| 16 | +``` | |
| 17 | + | |
| 18 | +## 작성 규칙 | |
| 19 | + | |
| 20 | +**1. 스타일은 `abstracts`만 `@use` 한다.** | |
| 21 | + | |
| 22 | +```scss | |
| 23 | +@use "@fox/styles/abstracts" as fox; | |
| 24 | + | |
| 25 | +.button { | |
| 26 | + padding: fox.space(2) fox.space(4); | |
| 27 | + border-radius: fox.radius(md); | |
| 28 | + background: fox.color(primary); | |
| 29 | + color: fox.color(on-primary); | |
| 30 | + | |
| 31 | + @include fox.typo(label-lg); | |
| 32 | + @include fox.focus-ring; | |
| 33 | + @include fox.transition((background-color, color)); | |
| 34 | +} | |
| 35 | +``` | |
| 36 | + | |
| 37 | +`@fox/styles`(진입점)를 `@use` 하면 안 됩니다 — 그쪽은 CSS를 출력하므로 컴포넌트마다 | |
| 38 | +토큰 선언 전체가 복제됩니다. `abstracts`는 출력이 0입니다. | |
| 39 | + | |
| 40 | +**2. 원시 디자인 값을 쓰지 않는다.** | |
| 41 | + | |
| 42 | +`color: #4f46e5`, `padding: 13px`, `border-radius: 12px` 금지. 전부 `fox.*()` 함수로 | |
| 43 | +토큰을 지목합니다. 없는 토큰 이름을 쓰면 빌드가 실패하므로 오타는 배포까지 갈 수 | |
| 44 | +없습니다. **맞는 토큰이 없다고 해서 원시 값을 쓰지 말고, 토큰을 먼저 추가**합니다 | |
| 45 | +(`@fox/styles/tokens/`). | |
| 46 | + | |
| 47 | +예외는 의미가 고정되어 토큰화가 무의미한 값뿐입니다 — `0`, `100%`, `1px`(구분선), | |
| 48 | +`auto`, `inherit`. | |
| 49 | + | |
| 50 | +**3. 기본은 Server Component로 둔다.** | |
| 51 | + | |
| 52 | +`'use client'`는 상호작용(상태·이벤트 핸들러·브라우저 API)이 실제로 필요한 컴포넌트에만 | |
| 53 | +붙입니다. CSS Module은 빌드타임에 클래스명 문자열로 바뀔 뿐이라 런타임 JS가 없으므로, | |
| 54 | +스타일링만 하는 컴포넌트는 서버에 남을 수 있습니다. 여기에 불필요하게 `'use client'`를 | |
| 55 | +붙이면 이 컴포넌트를 쓰는 화면 전체가 클라이언트로 내려갑니다. | |
| 56 | + | |
| 57 | +**4. variant는 클래스 맵으로 고정한다.** | |
| 58 | + | |
| 59 | +```tsx | |
| 60 | +import styles from "./button.module.scss"; | |
| 61 | +import { cx } from "@fox/core/utils"; | |
| 62 | + | |
| 63 | +type ButtonVariant = "primary" | "secondary" | "ghost"; | |
| 64 | + | |
| 65 | +const VARIANT: Record<ButtonVariant, string> = { | |
| 66 | + primary: styles.primary, | |
| 67 | + secondary: styles.secondary, | |
| 68 | + ghost: styles.ghost, | |
| 69 | +}; | |
| 70 | + | |
| 71 | +export function Button({ variant = "primary", className, ...props }: ButtonProps) { | |
| 72 | + return <button className={cx(styles.button, VARIANT[variant], className)} {...props} />; | |
| 73 | +} | |
| 74 | +``` | |
| 75 | + | |
| 76 | +`Record<Variant, string>`이라 variant를 추가하면 맵에 항목을 빠뜨릴 수 없습니다(타입 에러). | |
| 77 | + | |
| 78 | +**5. `className` prop은 배치용으로만 열어 둔다.** | |
| 79 | + | |
| 80 | +호출부가 `className`으로 넘기는 것은 margin·grid 배치 같은 **위치** 조정이지, 색·크기 같은 | |
| 81 | +**디자인 값**이 아닙니다. 디자인이 달라져야 하면 variant를 추가합니다. | |
| 82 | + | |
| 83 | +**6. 호스트 앱에 의존하지 않는다.** | |
| 84 | + | |
| 85 | +`@fox/` 안에서는 `@/lib/...`, `@/app/...` 같은 앱 경로를 import 하지 않습니다. 이 폴더는 | |
| 86 | +다른 프로젝트로 통째로 복사되어야 하므로, 앱이 주는 값은 반드시 prop이나 CSS 변수로 | |
| 87 | +받습니다(폰트가 `--app-font-sans`를 받는 것과 같은 방식). |
+++ @fox/core/utils/cx.ts
... | ... | @@ -0,0 +1,15 @@ |
| 1 | +export type ClassValue = string | false | null | undefined; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 조건부 클래스명을 합친다. falsy 값은 버린다. | |
| 5 | + * | |
| 6 | + * cx(styles.button, isActive && styles.active, className) | |
| 7 | + * | |
| 8 | + * 유틸리티 CSS 프레임워크에서 쓰는 클래스 병합 라이브러리(tailwind-merge 등)가 | |
| 9 | + * 필요 없는 이유: CSS Module은 클래스명이 파일 단위로 스코프되므로 서로 다른 | |
| 10 | + * 컴포넌트의 클래스가 같은 속성을 두고 충돌하지 않는다. 한 컴포넌트 안에서의 | |
| 11 | + * 충돌은 선언 순서(뒤에 온 규칙이 이김)로 결정되며, 이는 CSS의 정상 동작이다. | |
| 12 | + */ | |
| 13 | +export function cx(...values: ClassValue[]): string { | |
| 14 | + return values.filter(Boolean).join(" "); | |
| 15 | +} |
+++ @fox/core/utils/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { cx, type ClassValue } from "./cx"; |
+++ @fox/styles/_functions.scss
... | ... | @@ -0,0 +1,75 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점. | |
| 3 | +// | |
| 4 | +// 화면 코드는 `color: #4f46e5`나 `padding: 13px`를 쓸 수 없고, 반드시 | |
| 5 | +// `color: fox.color(primary)` / `padding: fox.space(3)`로 토큰을 지목해야 한다. | |
| 6 | +// 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패한다** — 오타나 임의 값이 | |
| 7 | +// 리뷰를 통과해 배포까지 흘러가는 경로가 원천 차단된다. | |
| 8 | +// | |
| 9 | +// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값 자체가 아니라 참조를 | |
| 10 | +// 돌려주므로 (1) 라이트/다크가 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는 | |
| 11 | +// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다. | |
| 12 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 13 | + | |
| 14 | +@use "sass:map"; | |
| 15 | +@use "tokens"; | |
| 16 | + | |
| 17 | +/// 토큰 이름을 검증하고 대응하는 CSS 커스텀 프로퍼티 참조를 반환한다. | |
| 18 | +/// @param {String} $group - 토큰 그룹(커스텀 프로퍼티 접두사에 그대로 쓰인다) | |
| 19 | +/// @param {String|Number} $name - 토큰 이름 | |
| 20 | +/// @param {Map} $registry - 검증 기준이 되는 토큰 map | |
| 21 | +@function _token($group, $name, $registry) { | |
| 22 | + @if not map.has-key($registry, $name) { | |
| 23 | + @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$name}` — 사용 가능한 값: #{map.keys($registry)}"; | |
| 24 | + } | |
| 25 | + | |
| 26 | + @return var(--fox-#{$group}-#{$name}); | |
| 27 | +} | |
| 28 | + | |
| 29 | +@function color($name) { | |
| 30 | + @return _token("color", $name, tokens.$color); | |
| 31 | +} | |
| 32 | + | |
| 33 | +@function space($name) { | |
| 34 | + @return _token("space", $name, tokens.$space); | |
| 35 | +} | |
| 36 | + | |
| 37 | +@function radius($name) { | |
| 38 | + @return _token("radius", $name, tokens.$radius); | |
| 39 | +} | |
| 40 | + | |
| 41 | +@function shadow($name) { | |
| 42 | + @return _token("shadow", $name, tokens.$shadow); | |
| 43 | +} | |
| 44 | + | |
| 45 | +@function duration($name) { | |
| 46 | + @return _token("duration", $name, tokens.$duration); | |
| 47 | +} | |
| 48 | + | |
| 49 | +@function easing($name) { | |
| 50 | + @return _token("easing", $name, tokens.$easing); | |
| 51 | +} | |
| 52 | + | |
| 53 | +@function font($name) { | |
| 54 | + @return _token("font", $name, tokens.$font-family); | |
| 55 | +} | |
| 56 | + | |
| 57 | +@function weight($name) { | |
| 58 | + @return _token("weight", $name, tokens.$font-weight); | |
| 59 | +} | |
| 60 | + | |
| 61 | +@function z($name) { | |
| 62 | + @return _token("z", $name, tokens.$z); | |
| 63 | +} | |
| 64 | + | |
| 65 | +/// 브레이크포인트 원값(rem 리터럴)을 반환한다 — **유일하게 `var()`가 아닌 실제 값을 | |
| 66 | +/// 돌려주는 함수**다. 미디어 쿼리가 `var()`를 해석하지 못하기 때문이다(`tokens/_layout.scss`). | |
| 67 | +/// 미디어 쿼리 자체는 `media()` 믹스인을 쓰고, 이 함수는 컨테이너 `max-width`처럼 | |
| 68 | +/// 브레이크포인트와 같은 값을 선언에 직접 써야 할 때만 쓴다. | |
| 69 | +@function bp($name) { | |
| 70 | + @if not map.has-key(tokens.$breakpoint, $name) { | |
| 71 | + @error "[@fox] 알 수 없는 breakpoint 토큰: `#{$name}` — 사용 가능한 값: #{map.keys(tokens.$breakpoint)}"; | |
| 72 | + } | |
| 73 | + | |
| 74 | + @return map.get(tokens.$breakpoint, $name); | |
| 75 | +} |
+++ @fox/styles/_mixins.scss
... | ... | @@ -0,0 +1,109 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 한 줄로 묶는다. | |
| 3 | +// | |
| 4 | +// 함수(`_functions.scss`)가 "값 하나"를 강제한다면, 믹스인은 "값들의 조합"을 | |
| 5 | +// 강제한다. 타이포 4종(크기·행간·굵기·자간)이나 감소 모션 분기처럼 하나만 빠져도 | |
| 6 | +// 어긋나는 것들을 화면마다 되풀이해 적지 않게 만드는 것이 목적이다. | |
| 7 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 8 | + | |
| 9 | +@use "sass:map"; | |
| 10 | +@use "tokens"; | |
| 11 | +@use "functions" as fn; | |
| 12 | + | |
| 13 | +/// 타이포 프리셋 적용 — 크기·행간·굵기·자간 4종을 한 번에. | |
| 14 | +/// 개별 스칼라를 따로 고르지 못하게 해서 타이포 조합이 무한히 갈라지는 것을 막는다. | |
| 15 | +@mixin typo($name) { | |
| 16 | + @if not map.has-key(tokens.$typo, $name) { | |
| 17 | + @error "[@fox] 알 수 없는 typo 프리셋: `#{$name}` — 사용 가능한 값: #{map.keys(tokens.$typo)}"; | |
| 18 | + } | |
| 19 | + | |
| 20 | + font-size: var(--fox-typo-#{$name}-size); | |
| 21 | + font-weight: var(--fox-typo-#{$name}-weight); | |
| 22 | + line-height: var(--fox-typo-#{$name}-line-height); | |
| 23 | + letter-spacing: var(--fox-typo-#{$name}-letter-spacing); | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// 지정 브레이크포인트 **이상**에서 적용 (모바일 우선). | |
| 27 | +@mixin media($name) { | |
| 28 | + @media (min-width: fn.bp($name)) { | |
| 29 | + @content; | |
| 30 | + } | |
| 31 | +} | |
| 32 | + | |
| 33 | +/// 지정 브레이크포인트 **미만**에서 적용. | |
| 34 | +/// `media()`와 경계가 겹치지 않도록 0.01rem을 뺀다 — 같은 뷰포트 폭에서 두 블록이 | |
| 35 | +/// 동시에 적용되면 나중 선언이 이기는 미묘한 버그가 생긴다. | |
| 36 | +@mixin media-below($name) { | |
| 37 | + @media (max-width: fn.bp($name) - 0.01rem) { | |
| 38 | + @content; | |
| 39 | + } | |
| 40 | +} | |
| 41 | + | |
| 42 | +/// 트랜지션 — `prefers-reduced-motion: no-preference`로 감싸 감소 모션 환경에서는 | |
| 43 | +/// 아예 선언되지 않게 한다(값을 0으로 만드는 것보다 확실하다). | |
| 44 | +/// @param {List} $properties - 전환할 속성. 여러 개면 괄호로 묶어 전달한다. | |
| 45 | +@mixin transition($properties, $duration: base, $easing: out) { | |
| 46 | + @media (prefers-reduced-motion: no-preference) { | |
| 47 | + transition-property: $properties; | |
| 48 | + transition-duration: fn.duration($duration); | |
| 49 | + transition-timing-function: fn.easing($easing); | |
| 50 | + } | |
| 51 | +} | |
| 52 | + | |
| 53 | +/// 키프레임 애니메이션 — 감소 모션 분기는 `transition()`과 동일하게 내장. | |
| 54 | +@mixin animate($name, $duration: base, $easing: out, $fill: both) { | |
| 55 | + @media (prefers-reduced-motion: no-preference) { | |
| 56 | + animation: $name fn.duration($duration) fn.easing($easing) $fill; | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +/// 키보드 포커스 링 — `:focus-visible`만 대상으로 하므로 마우스 클릭 시에는 | |
| 61 | +/// 나타나지 않는다. 모든 상호작용 요소에 반드시 넣는다(접근성). | |
| 62 | +@mixin focus-ring($color: null, $width: 0.2rem, $offset: 0.2rem) { | |
| 63 | + $ring-color: fn.color(primary); | |
| 64 | + | |
| 65 | + @if $color != null { | |
| 66 | + $ring-color: $color; | |
| 67 | + } | |
| 68 | + | |
| 69 | + &:focus-visible { | |
| 70 | + outline: $width solid $ring-color; | |
| 71 | + outline-offset: $offset; | |
| 72 | + } | |
| 73 | +} | |
| 74 | + | |
| 75 | +/// 화면에는 보이지 않지만 스크린리더는 읽는 텍스트. | |
| 76 | +/// `display: none`이나 `visibility: hidden`은 보조기기에서도 사라지므로 쓰지 않는다. | |
| 77 | +@mixin visually-hidden { | |
| 78 | + position: absolute; | |
| 79 | + width: 1px; | |
| 80 | + height: 1px; | |
| 81 | + margin: -1px; | |
| 82 | + padding: 0; | |
| 83 | + overflow: hidden; | |
| 84 | + clip-path: inset(50%); | |
| 85 | + white-space: nowrap; | |
| 86 | + border: 0; | |
| 87 | +} | |
| 88 | + | |
| 89 | +/// 말줄임 — 1줄이면 `text-overflow`, 여러 줄이면 line-clamp. | |
| 90 | +@mixin truncate($lines: 1) { | |
| 91 | + overflow: hidden; | |
| 92 | + | |
| 93 | + @if $lines == 1 { | |
| 94 | + text-overflow: ellipsis; | |
| 95 | + white-space: nowrap; | |
| 96 | + } @else { | |
| 97 | + display: -webkit-box; | |
| 98 | + -webkit-box-orient: vertical; | |
| 99 | + -webkit-line-clamp: $lines; | |
| 100 | + } | |
| 101 | +} | |
| 102 | + | |
| 103 | +/// 페이지 콘텐츠 가로 폭 제한 + 좌우 여백. | |
| 104 | +@mixin container($max: xl, $gutter: 4) { | |
| 105 | + width: 100%; | |
| 106 | + max-width: fn.bp($max); | |
| 107 | + margin-inline: auto; | |
| 108 | + padding-inline: fn.space($gutter); | |
| 109 | +} |
+++ @fox/styles/_reset.scss
... | ... | @@ -0,0 +1,121 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 리셋 & 베이스 | |
| 3 | +// | |
| 4 | +// 브라우저 기본 스타일 중 디자인 시스템과 충돌하는 것만 걷어낸다. 전면 리셋(모든 | |
| 5 | +// 요소를 무스타일로)이 아니라, 시맨틱 태그의 기본 동작은 살리고 시각 표현만 | |
| 6 | +// 토큰으로 되돌리는 방식이다. | |
| 7 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 8 | + | |
| 9 | +@use "abstracts" as fox; | |
| 10 | + | |
| 11 | +*, | |
| 12 | +*::before, | |
| 13 | +*::after { | |
| 14 | + box-sizing: border-box; | |
| 15 | +} | |
| 16 | + | |
| 17 | +// ⚠️ **1rem = 10px 규약의 근거.** 브라우저 기본 글자 크기(16px)의 62.5%가 10px다. | |
| 18 | +// px → rem 환산이 10으로 나누기가 되어 토큰 값이 읽기 쉬워진다. | |
| 19 | +// px 고정과 달리 사용자가 브라우저 글자 크기를 키우면 앱 전체가 비례해 확대된다 | |
| 20 | +// (접근성 설정 존중). 그 대신 `body`에서 실제 본문 크기를 되돌려 놓아야 한다 — | |
| 21 | +// 아래 `body`의 `typo(body-md)`가 그 역할이다. | |
| 22 | +// | |
| 23 | +// ⚠️ 미디어 쿼리 안의 `rem`은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 | |
| 24 | +// 기준). `tokens/_layout.scss` 참고. | |
| 25 | +html { | |
| 26 | + font-size: 62.5%; | |
| 27 | + -webkit-text-size-adjust: 100%; | |
| 28 | +} | |
| 29 | + | |
| 30 | +body { | |
| 31 | + margin: 0; | |
| 32 | + background-color: fox.color(background); | |
| 33 | + color: fox.color(foreground); | |
| 34 | + font-family: fox.font(sans); | |
| 35 | + -webkit-font-smoothing: antialiased; | |
| 36 | + -moz-osx-font-smoothing: grayscale; | |
| 37 | + | |
| 38 | + @include fox.typo(body-md); | |
| 39 | +} | |
| 40 | + | |
| 41 | +// 제목·문단의 기본 여백을 제거한다 — 간격은 레이아웃(gap/margin 토큰)이 책임지고, | |
| 42 | +// 태그가 스스로 여백을 갖지 않게 해야 조립 결과를 예측할 수 있다. | |
| 43 | +h1, | |
| 44 | +h2, | |
| 45 | +h3, | |
| 46 | +h4, | |
| 47 | +h5, | |
| 48 | +h6, | |
| 49 | +p, | |
| 50 | +figure, | |
| 51 | +blockquote, | |
| 52 | +dl, | |
| 53 | +dd { | |
| 54 | + margin: 0; | |
| 55 | +} | |
| 56 | + | |
| 57 | +// 제목 태그의 기본 크기/굵기도 제거한다 — 시각적 위계는 `typo()` 프리셋으로만 | |
| 58 | +// 정하고, 태그 선택은 문서 구조(접근성) 기준으로 하게 만든다. | |
| 59 | +h1, | |
| 60 | +h2, | |
| 61 | +h3, | |
| 62 | +h4, | |
| 63 | +h5, | |
| 64 | +h6 { | |
| 65 | + font-size: inherit; | |
| 66 | + font-weight: inherit; | |
| 67 | +} | |
| 68 | + | |
| 69 | +ul, | |
| 70 | +ol { | |
| 71 | + margin: 0; | |
| 72 | + padding: 0; | |
| 73 | + list-style: none; | |
| 74 | +} | |
| 75 | + | |
| 76 | +// 폼 요소는 폰트를 상속하지 않는 것이 브라우저 기본값이라 명시적으로 되돌린다. | |
| 77 | +button, | |
| 78 | +input, | |
| 79 | +select, | |
| 80 | +textarea { | |
| 81 | + margin: 0; | |
| 82 | + font: inherit; | |
| 83 | + color: inherit; | |
| 84 | + letter-spacing: inherit; | |
| 85 | +} | |
| 86 | + | |
| 87 | +button { | |
| 88 | + padding: 0; | |
| 89 | + border: 0; | |
| 90 | + background: none; | |
| 91 | + cursor: pointer; | |
| 92 | +} | |
| 93 | + | |
| 94 | +button:disabled { | |
| 95 | + cursor: default; | |
| 96 | +} | |
| 97 | + | |
| 98 | +a { | |
| 99 | + color: inherit; | |
| 100 | + text-decoration: none; | |
| 101 | +} | |
| 102 | + | |
| 103 | +img, | |
| 104 | +svg, | |
| 105 | +video, | |
| 106 | +canvas { | |
| 107 | + display: block; | |
| 108 | + max-width: 100%; | |
| 109 | +} | |
| 110 | + | |
| 111 | +table { | |
| 112 | + border-collapse: collapse; | |
| 113 | + border-spacing: 0; | |
| 114 | +} | |
| 115 | + | |
| 116 | +// 브라우저 기본 포커스 링을 지우지 않는다 — 컴포넌트가 `focus-ring()` 믹스인으로 | |
| 117 | +// 자기 링을 정의하면 그것이 우선하고, 정의하지 않은 요소는 기본 링이 남아 키보드 | |
| 118 | +// 사용자가 길을 잃지 않는다. | |
| 119 | +:where(:focus-visible) { | |
| 120 | + outline-offset: 0.2rem; | |
| 121 | +} |
+++ @fox/styles/_root.scss
... | ... | @@ -0,0 +1,104 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 토큰 CSS 출력 — `tokens/` 의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다. | |
| 3 | +// | |
| 4 | +// **이 파일은 앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유). | |
| 5 | +// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다 — CSS Module은 파일마다 | |
| 6 | +// 별도 컴파일 단위여서, 컴포넌트마다 `:root` 블록 전체가 복제된다. | |
| 7 | +// | |
| 8 | +// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다. 값의 이름·개수는 | |
| 9 | +// 언제나 `tokens/` 쪽 map 하나에만 존재한다. | |
| 10 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 11 | + | |
| 12 | +@use "sass:map"; | |
| 13 | +@use "tokens"; | |
| 14 | + | |
| 15 | +:root { | |
| 16 | + // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을 | |
| 17 | + // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다. | |
| 18 | + color-scheme: light dark; | |
| 19 | + | |
| 20 | + @each $name, $value in tokens.$color { | |
| 21 | + --fox-color-#{$name}: #{$value}; | |
| 22 | + } | |
| 23 | + | |
| 24 | + @each $name, $value in tokens.$space { | |
| 25 | + --fox-space-#{$name}: #{$value}; | |
| 26 | + } | |
| 27 | + | |
| 28 | + @each $name, $value in tokens.$radius { | |
| 29 | + --fox-radius-#{$name}: #{$value}; | |
| 30 | + } | |
| 31 | + | |
| 32 | + @each $name, $value in tokens.$shadow { | |
| 33 | + --fox-shadow-#{$name}: #{$value}; | |
| 34 | + } | |
| 35 | + | |
| 36 | + @each $name, $value in tokens.$duration { | |
| 37 | + --fox-duration-#{$name}: #{$value}; | |
| 38 | + } | |
| 39 | + | |
| 40 | + @each $name, $value in tokens.$easing { | |
| 41 | + --fox-easing-#{$name}: #{$value}; | |
| 42 | + } | |
| 43 | + | |
| 44 | + @each $name, $value in tokens.$font-family { | |
| 45 | + --fox-font-#{$name}: #{$value}; | |
| 46 | + } | |
| 47 | + | |
| 48 | + @each $name, $value in tokens.$font-weight { | |
| 49 | + --fox-weight-#{$name}: #{$value}; | |
| 50 | + } | |
| 51 | + | |
| 52 | + @each $name, $value in tokens.$z { | |
| 53 | + --fox-z-#{$name}: #{$value}; | |
| 54 | + } | |
| 55 | + | |
| 56 | + // 브레이크포인트는 **미디어 쿼리에서 쓸 수 없다** — 조건부는 `var()`를 해석하지 | |
| 57 | + // 않는다. 그럼에도 내보내는 이유는 컨테이너 `max-width`처럼 선언부에서 같은 값을 | |
| 58 | + // 참조할 일이 있고, 카탈로그가 토큰 목록을 CSSOM에서 읽기 때문이다. | |
| 59 | + // 미디어 쿼리는 언제나 `fox.media()` 믹스인을 쓴다. | |
| 60 | + @each $name, $value in tokens.$breakpoint { | |
| 61 | + --fox-breakpoint-#{$name}: #{$value}; | |
| 62 | + } | |
| 63 | + | |
| 64 | + // 타이포 프리셋은 한 이름당 4개의 프로퍼티로 펼쳐진다 — `fox.typo()` 믹스인이 | |
| 65 | + // 이 네 개를 함께 참조한다. | |
| 66 | + @each $name, $preset in tokens.$typo { | |
| 67 | + --fox-typo-#{$name}-size: #{map.get($preset, size)}; | |
| 68 | + --fox-typo-#{$name}-weight: #{map.get($preset, weight)}; | |
| 69 | + --fox-typo-#{$name}-line-height: #{map.get($preset, line-height)}; | |
| 70 | + --fox-typo-#{$name}-letter-spacing: #{map.get($preset, letter-spacing)}; | |
| 71 | + } | |
| 72 | +} | |
| 73 | + | |
| 74 | +// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다. | |
| 75 | +// 색상 값을 여기서 재정의하지 않으므로 라이트/다크 값이 어긋날 수 없다. | |
| 76 | +// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다. | |
| 77 | +:root[data-theme="light"] { | |
| 78 | + color-scheme: light; | |
| 79 | +} | |
| 80 | + | |
| 81 | +:root[data-theme="dark"] { | |
| 82 | + color-scheme: dark; | |
| 83 | +} | |
| 84 | + | |
| 85 | +// 테마 전환 애니메이션 — 호스트 앱이 토글 직전 `<html>`에 `[data-theme-transition]`을 | |
| 86 | +// 부여하고 ~200ms 후 제거하는 **시간 한정 opt-in**이다. 상시 트랜지션이 아니므로 | |
| 87 | +// 다른 hover 트랜지션과 경쟁하지 않는다. | |
| 88 | +// `prefers-reduced-motion` 게이팅으로 감소 모션 환경에서는 즉시 반영된다. | |
| 89 | +// `!important`는 이 200ms 동안만 존재하는 규칙이 컴포넌트의 transition 선언에 | |
| 90 | +// 밀리지 않게 하기 위한 것이다. | |
| 91 | +@media (prefers-reduced-motion: no-preference) { | |
| 92 | + html[data-theme-transition], | |
| 93 | + html[data-theme-transition] *, | |
| 94 | + html[data-theme-transition] *::before, | |
| 95 | + html[data-theme-transition] *::after { | |
| 96 | + transition: | |
| 97 | + background-color var(--fox-duration-base) var(--fox-easing-out), | |
| 98 | + border-color var(--fox-duration-base) var(--fox-easing-out), | |
| 99 | + color var(--fox-duration-base) var(--fox-easing-out), | |
| 100 | + fill var(--fox-duration-base) var(--fox-easing-out), | |
| 101 | + stroke var(--fox-duration-base) var(--fox-easing-out), | |
| 102 | + outline-color var(--fox-duration-base) var(--fox-easing-out) !important; | |
| 103 | + } | |
| 104 | +} |
+++ @fox/styles/abstracts.scss
... | ... | @@ -0,0 +1,22 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일. | |
| 3 | +// | |
| 4 | +// @use "@fox/styles/abstracts" as fox; | |
| 5 | +// | |
| 6 | +// .button { | |
| 7 | +// padding: fox.space(2) fox.space(4); | |
| 8 | +// background: fox.color(primary); | |
| 9 | +// border-radius: fox.radius(md); | |
| 10 | +// @include fox.typo(label-lg); | |
| 11 | +// @include fox.focus-ring; | |
| 12 | +// } | |
| 13 | +// | |
| 14 | +// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다. | |
| 15 | +// CSS Module은 파일마다 별도 컴파일 단위라, CSS를 출력하는 모듈을 여기서 forward하면 | |
| 16 | +// 컴포넌트 수만큼 같은 CSS가 복제된다. 실제 CSS 출력(토큰 선언·리셋)은 앱이 딱 한 번 | |
| 17 | +// import하는 `index.scss`가 전담한다. | |
| 18 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 19 | + | |
| 20 | +@forward "tokens"; | |
| 21 | +@forward "functions"; | |
| 22 | +@forward "mixins"; |
+++ @fox/styles/index.scss
... | ... | @@ -0,0 +1,13 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.** | |
| 3 | +// | |
| 4 | +// // app/globals.scss | |
| 5 | +// @use "@fox/styles"; | |
| 6 | +// | |
| 7 | +// 여기서만 CSS가 출력된다(토큰 선언 + 리셋). 컴포넌트의 `.module.scss`는 이 파일이 | |
| 8 | +// 아니라 `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 | |
| 9 | +// 참조해도 중복이 생기지 않는다. | |
| 10 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 11 | + | |
| 12 | +@use "root"; | |
| 13 | +@use "reset"; |
+++ @fox/styles/tokens/_color.scss
... | ... | @@ -0,0 +1,61 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 색상 토큰 | |
| 3 | +// | |
| 4 | +// 이 map이 단일 진실 공급원(SSOT)이다. 여기서 두 가지가 함께 파생된다: | |
| 5 | +// 1. `_root.scss` → `--fox-color-*` CSS 커스텀 프로퍼티 선언 | |
| 6 | +// 2. `_functions.scss`의 `color()` → 토큰 이름 유효성 컴파일타임 검증 | |
| 7 | +// 값과 이름 목록이 한 파일에만 존재하므로 둘 사이의 드리프트가 구조적으로 불가능하다. | |
| 8 | +// | |
| 9 | +// 라이트/다크는 `light-dark(라이트값, 다크값)` 한 줄로 함께 보유한다. 테마 전환은 | |
| 10 | +// `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록을 따로 두지 않는다 — | |
| 11 | +// 한쪽만 고쳐서 생기는 테마 불일치가 애초에 발생할 수 없다. | |
| 12 | +// | |
| 13 | +// 브라우저 지원: `light-dark()` 자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이 | |
| 14 | +// 필요하지만, Next.js 16(Turbopack)의 Lightning CSS가 현재 설정에서 이를 | |
| 15 | +// `var(--lightningcss-light, 라이트) var(--lightningcss-dark, 다크)` 조합으로 **폴리필**해 | |
| 16 | +// 출력하므로 구형 브라우저에서도 동작한다(2026-08-11 빌드 산출물에서 확인). | |
| 17 | +// browserslist 타깃을 최신 브라우저로 좁히면 폴리필이 빠지고 원본 `light-dark()`가 | |
| 18 | +// 그대로 나가므로, 타깃을 바꿀 때 이 전제를 다시 확인한다. | |
| 19 | +// | |
| 20 | +// ⚠️ 아래 값은 전부 **임시 플레이스홀더**다. 파이프라인 검증과 /design 카탈로그 | |
| 21 | +// 렌더를 위한 중립 팔레트이며, 실제 브랜드 값은 사용자 제공 Figma/파일로 교체한다. | |
| 22 | +// 교체 시 이 파일의 값만 바꾸면 앱 전체에 반영된다. | |
| 23 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 24 | + | |
| 25 | +$color: ( | |
| 26 | + // 브랜드 | |
| 27 | + primary: light-dark(#4f46e5, #818cf8), | |
| 28 | + primary-hover: light-dark(#4338ca, #a5b4fc), | |
| 29 | + primary-subtle: light-dark(#eef2ff, #312e81), | |
| 30 | + on-primary: light-dark(#ffffff, #1e1b34), | |
| 31 | + | |
| 32 | + // 표면 — background(페이지 바닥) < surface(카드) < surface-raised(팝오버 등) | |
| 33 | + background: light-dark(#ffffff, #14161b), | |
| 34 | + surface: light-dark(#ffffff, #1e2128), | |
| 35 | + surface-muted: light-dark(#f5f7fa, #262a33), | |
| 36 | + surface-raised: light-dark(#ffffff, #2c3038), | |
| 37 | + | |
| 38 | + // 전경(텍스트·아이콘) | |
| 39 | + foreground: light-dark(#1f2430, #f5f7fa), | |
| 40 | + foreground-muted: light-dark(#565d6b, #bbc4d8), | |
| 41 | + foreground-subtle: light-dark(#8790a3, #8790a3), | |
| 42 | + foreground-inverse: light-dark(#ffffff, #14161b), | |
| 43 | + | |
| 44 | + // 경계선 | |
| 45 | + border: light-dark(#dae0ed, #424752), | |
| 46 | + border-subtle: light-dark(#eaeef6, #2f343d), | |
| 47 | + border-strong: light-dark(#b6bfd2, #5a616e), | |
| 48 | + | |
| 49 | + // 상태 — 각 상태색 위에 얹는 텍스트색을 on-* 로 함께 정의한다. | |
| 50 | + danger: light-dark(#dc2626, #f87171), | |
| 51 | + on-danger: light-dark(#ffffff, #1a0d0d), | |
| 52 | + success: light-dark(#16a34a, #4ade80), | |
| 53 | + on-success: light-dark(#ffffff, #0a1a10), | |
| 54 | + warning: light-dark(#d97706, #fbbf24), | |
| 55 | + on-warning: light-dark(#ffffff, #1a1205), | |
| 56 | + info: light-dark(#0284c7, #38bdf8), | |
| 57 | + on-info: light-dark(#ffffff, #05161f), | |
| 58 | + | |
| 59 | + // 오버레이 | |
| 60 | + scrim: light-dark(rgb(0 0 0 / 0.5), rgb(0 0 0 / 0.65)) | |
| 61 | +); |
+++ @fox/styles/tokens/_index.scss
... | ... | @@ -0,0 +1,13 @@ |
| 1 | +// 토큰 map 모음 진입점 — **CSS를 한 줄도 출력하지 않는다.** | |
| 2 | +// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의 | |
| 3 | +// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 | |
| 4 | +// 않는다(각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 | |
| 5 | +// 파일마다 같은 CSS가 복제된다). | |
| 6 | + | |
| 7 | +@forward "color"; | |
| 8 | +@forward "space"; | |
| 9 | +@forward "typography"; | |
| 10 | +@forward "radius"; | |
| 11 | +@forward "shadow"; | |
| 12 | +@forward "motion"; | |
| 13 | +@forward "layout"; |
+++ @fox/styles/tokens/_layout.scss
... | ... | @@ -0,0 +1,41 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 레이아웃 토큰 — 브레이크포인트와 z-index 층. | |
| 3 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 4 | + | |
| 5 | +// 브레이크포인트는 이 파일에서 **유일하게 CSS 커스텀 프로퍼티로 내보내지 않는 | |
| 6 | +// 토큰**이다. 미디어 쿼리 조건부는 `var()`를 해석하지 않기 때문에 | |
| 7 | +// (`@media (min-width: var(--x))`는 무효), 컴파일타임 SCSS 값이어야만 한다. | |
| 8 | +// 그래서 브레이크포인트 사용은 `fox.media()` 믹스인 경유가 강제된다. | |
| 9 | +// | |
| 10 | +// ⚠️ **미디어 쿼리 안의 `rem`은 브라우저 기본 글자 크기(보통 16px) 기준이다.** | |
| 11 | +// `html { font-size: 62.5% }`의 영향을 받지 않으므로, 다른 토큰의 "1rem = 10px" | |
| 12 | +// 규칙이 여기서는 적용되지 않는다. 아래 px 환산을 반드시 함께 본다. | |
| 13 | +// rem으로 두는 이유: 사용자가 브라우저 글자 크기를 키우면 브레이크포인트도 함께 | |
| 14 | +// 커져 확대 환경에서 레이아웃이 더 일찍 접힌다(px 고정은 이 대응이 불가능하다). | |
| 15 | +$breakpoint: ( | |
| 16 | + sm: 40rem, | |
| 17 | + // 640px | |
| 18 | + md: 48rem, | |
| 19 | + // 768px | |
| 20 | + lg: 64rem, | |
| 21 | + // 1024px | |
| 22 | + xl: 80rem, | |
| 23 | + // 1280px | |
| 24 | + 2xl: 96rem | |
| 25 | + // 1536px | |
| 26 | +); | |
| 27 | + | |
| 28 | +// z-index 층 — 숫자를 화면에서 직접 고르지 않게 하려는 토큰이다. 값 사이를 100씩 | |
| 29 | +// 띄워 둔 것은 나중에 층 사이에 새 층을 끼워 넣을 여지를 남기기 위함이다. | |
| 30 | +$z: ( | |
| 31 | + base: 0, | |
| 32 | + raised: 100, | |
| 33 | + sticky: 200, | |
| 34 | + dropdown: 300, | |
| 35 | + scrim: 400, | |
| 36 | + modal: 500, | |
| 37 | + popover: 600, | |
| 38 | + toast: 700, | |
| 39 | + tooltip: 800, | |
| 40 | + splash: 900 | |
| 41 | +); |
+++ @fox/styles/tokens/_motion.scss
... | ... | @@ -0,0 +1,26 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 모션 토큰 — 지속시간과 이징. | |
| 3 | +// 이징은 "어느 방향으로 움직이는가"로 이름 붙인다: 화면에 들어올 때는 감속(out), | |
| 4 | +// 나갈 때는 가속(in), 제자리에서 상태만 바뀔 때는 in-out. | |
| 5 | +// | |
| 6 | +// 실제 트랜지션·애니메이션은 `_mixins.scss`의 `transition()` / `animate()`를 쓴다 — | |
| 7 | +// 두 믹스인 모두 `prefers-reduced-motion` 분기를 내장하고 있어 감소 모션 환경 | |
| 8 | +// 대응을 화면마다 되풀이할 필요가 없다. | |
| 9 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 10 | + | |
| 11 | +$duration: ( | |
| 12 | + instant: 0ms, | |
| 13 | + fast: 120ms, | |
| 14 | + base: 200ms, | |
| 15 | + slow: 300ms, | |
| 16 | + slower: 500ms | |
| 17 | +); | |
| 18 | + | |
| 19 | +$easing: ( | |
| 20 | + linear: linear, | |
| 21 | + in: cubic-bezier(0.4, 0, 1, 1), | |
| 22 | + out: cubic-bezier(0, 0, 0.2, 1), | |
| 23 | + in-out: cubic-bezier(0.4, 0, 0.2, 1), | |
| 24 | + // 살짝 오버슈트 — 토글·체크 같은 작은 확인 동작에. | |
| 25 | + spring: cubic-bezier(0.34, 1.56, 0.64, 1) | |
| 26 | +); |
+++ @fox/styles/tokens/_radius.scss
... | ... | @@ -0,0 +1,17 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 모서리 반경 토큰 — 1rem = 10px 기준. | |
| 3 | +// `full`은 pill 형태(캡슐 버튼·뱃지)를 위한 값으로, 높이보다 크기만 하면 되므로 | |
| 4 | +// 임의의 큰 값을 쓴다. | |
| 5 | +// ⚠️ 값은 임시 플레이스홀더 — Figma 확정 시 교체. | |
| 6 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 7 | + | |
| 8 | +$radius: ( | |
| 9 | + none: 0, | |
| 10 | + xs: 0.4rem, | |
| 11 | + sm: 0.6rem, | |
| 12 | + md: 0.8rem, | |
| 13 | + lg: 1.2rem, | |
| 14 | + xl: 1.6rem, | |
| 15 | + 2xl: 2.4rem, | |
| 16 | + full: 999rem | |
| 17 | +); |
+++ @fox/styles/tokens/_shadow.scss
... | ... | @@ -0,0 +1,18 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 그림자 토큰 — 고도(elevation)를 역할 이름으로 표현한다. | |
| 3 | +// 크기(sm/md/lg)가 아니라 쓰임(card/dialog/…)으로 이름 붙이면 "이 요소는 어느 | |
| 4 | +// 층에 뜨는가"가 코드에 드러나고, 나중에 값이 바뀌어도 의미가 유지된다. | |
| 5 | +// | |
| 6 | +// 그림자는 라이트/다크에서 같은 값을 쓰지 않는 편이 자연스럽다(어두운 배경에서는 | |
| 7 | +// 그림자가 거의 보이지 않는다). 필요해지면 여기서 `light-dark()`로 분기한다. | |
| 8 | +// ⚠️ 값은 임시 플레이스홀더 — Figma 확정 시 교체. | |
| 9 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 10 | + | |
| 11 | +$shadow: ( | |
| 12 | + none: none, | |
| 13 | + card: light-dark(0 0.1rem 0.3rem rgb(16 24 40 / 0.08), 0 0.1rem 0.3rem rgb(0 0 0 / 0.4)), | |
| 14 | + raised: light-dark(0 0.4rem 0.8rem rgb(16 24 40 / 0.1), 0 0.4rem 0.8rem rgb(0 0 0 / 0.45)), | |
| 15 | + popup: light-dark(0 0.8rem 1.6rem rgb(16 24 40 / 0.12), 0 0.8rem 1.6rem rgb(0 0 0 / 0.5)), | |
| 16 | + dialog: light-dark(0 1.6rem 3.2rem rgb(16 24 40 / 0.16), 0 1.6rem 3.2rem rgb(0 0 0 / 0.55)), | |
| 17 | + toast: light-dark(0 1rem 2rem rgb(16 24 40 / 0.18), 0 1rem 2rem rgb(0 0 0 / 0.6)) | |
| 18 | +); |
+++ @fox/styles/tokens/_space.scss
... | ... | @@ -0,0 +1,29 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 간격 토큰 (4/8 grid) | |
| 3 | +// | |
| 4 | +// **모든 치수는 rem 기준이며 `1rem = 10px`이다** (`_reset.scss`의 | |
| 5 | +// `html { font-size: 62.5% }`). px → rem 환산이 10으로 나누기라 암산이 되고, | |
| 6 | +// 사용자가 브라우저 기본 글자 크기를 키우면 레이아웃 전체가 함께 확대된다 | |
| 7 | +// (px 고정과 달리 접근성 설정을 존중한다). | |
| 8 | +// | |
| 9 | +// 키는 4px 배수 단위다 — `space(4)` = 16px. 임의 값이 필요해 보이면 스케일에 | |
| 10 | +// 단계를 추가할 일인지 먼저 검토한다(§10.4). | |
| 11 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 12 | + | |
| 13 | +$space: ( | |
| 14 | + 0: 0, | |
| 15 | + 1: 0.4rem, | |
| 16 | + 2: 0.8rem, | |
| 17 | + 3: 1.2rem, | |
| 18 | + 4: 1.6rem, | |
| 19 | + 5: 2rem, | |
| 20 | + 6: 2.4rem, | |
| 21 | + 8: 3.2rem, | |
| 22 | + 10: 4rem, | |
| 23 | + 12: 4.8rem, | |
| 24 | + 16: 6.4rem, | |
| 25 | + 20: 8rem, | |
| 26 | + 24: 9.6rem | |
| 27 | +); | |
| 28 | +// 키 → px 환산: 1=4 · 2=8 · 3=12 · 4=16 · 5=20 · 6=24 · 8=32 · 10=40 · 12=48 · | |
| 29 | +// 16=64 · 20=80 · 24=96 |
+++ @fox/styles/tokens/_typography.scss
... | ... | @@ -0,0 +1,52 @@ |
| 1 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | +// 타이포그래피 토큰 | |
| 3 | +// | |
| 4 | +// 폰트 패밀리는 **호스트 앱이 주입하는 계약**이다 — `@fox`는 `--app-font-sans` / | |
| 5 | +// `--app-font-mono`를 참조만 하고 폰트 파일을 소유하지 않는다. Next.js는 `next/font`가 | |
| 6 | +// 앱의 layout에서 CSS 변수를 만들어 주므로(폰트 최적화·자체 호스팅이 앱 책임), | |
| 7 | +// `@fox` 폴더를 다른 프로젝트로 복사해도 그 프로젝트의 폰트를 그대로 따른다. | |
| 8 | +// 미주입 시에도 깨지지 않도록 `var()` 폴백을 반드시 둔다 — 폴백 없는 `var()`가 | |
| 9 | +// undefined면 font-family 선언 전체가 무효가 된다. | |
| 10 | +// | |
| 11 | +// 개별 스칼라(size/weight/…)보다 **프리셋(`$typo`)을 우선 사용**한다. 크기·행간· | |
| 12 | +// 자간·굵기는 함께 움직여야 의도한 타이포가 되므로, 화면에서 넷을 따로 고르게 두면 | |
| 13 | +// 조합이 무한히 갈라진다. `@include fox.typo(title-md)` 한 줄로 넷이 함께 적용된다. | |
| 14 | +// | |
| 15 | +// ⚠️ 아래 값·이름은 임시 플레이스홀더다. 실제 타입 스케일은 Figma 확정 시 교체한다. | |
| 16 | +// ───────────────────────────────────────────────────────────────────────────── | |
| 17 | + | |
| 18 | +$font-family: ( | |
| 19 | + sans: (var(--app-font-sans, system-ui), "Apple SD Gothic Neo", sans-serif), | |
| 20 | + mono: (var(--app-font-mono, ui-monospace), monospace) | |
| 21 | +); | |
| 22 | + | |
| 23 | +$font-weight: ( | |
| 24 | + regular: 400, | |
| 25 | + medium: 500, | |
| 26 | + semibold: 600, | |
| 27 | + bold: 700 | |
| 28 | +); | |
| 29 | + | |
| 30 | +// 타이포 프리셋 — size / line-height / weight / letter-spacing 4종을 한 묶음으로. | |
| 31 | +// 1rem = 10px 기준이므로 1.6rem = 16px. | |
| 32 | +$typo: ( | |
| 33 | + display-lg: (size: 4.8rem, line-height: 1.2, weight: 700, letter-spacing: -0.02em), | |
| 34 | + display-md: (size: 4rem, line-height: 1.2, weight: 700, letter-spacing: -0.02em), | |
| 35 | + display-sm: (size: 3.2rem, line-height: 1.25, weight: 700, letter-spacing: -0.02em), | |
| 36 | + | |
| 37 | + headline-lg: (size: 2.8rem, line-height: 1.3, weight: 700, letter-spacing: -0.015em), | |
| 38 | + headline-md: (size: 2.4rem, line-height: 1.35, weight: 700, letter-spacing: -0.015em), | |
| 39 | + headline-sm: (size: 2rem, line-height: 1.4, weight: 700, letter-spacing: -0.01em), | |
| 40 | + | |
| 41 | + title-lg: (size: 1.8rem, line-height: 1.45, weight: 600, letter-spacing: -0.01em), | |
| 42 | + title-md: (size: 1.6rem, line-height: 1.5, weight: 600, letter-spacing: 0), | |
| 43 | + title-sm: (size: 1.4rem, line-height: 1.5, weight: 600, letter-spacing: 0), | |
| 44 | + | |
| 45 | + body-lg: (size: 1.6rem, line-height: 1.6, weight: 400, letter-spacing: 0), | |
| 46 | + body-md: (size: 1.4rem, line-height: 1.6, weight: 400, letter-spacing: 0), | |
| 47 | + body-sm: (size: 1.2rem, line-height: 1.55, weight: 400, letter-spacing: 0), | |
| 48 | + | |
| 49 | + label-lg: (size: 1.4rem, line-height: 1.4, weight: 600, letter-spacing: 0), | |
| 50 | + label-md: (size: 1.3rem, line-height: 1.4, weight: 500, letter-spacing: 0), | |
| 51 | + label-sm: (size: 1.2rem, line-height: 1.35, weight: 500, letter-spacing: 0.01em) | |
| 52 | +); |
+++ app/design/_components/theme-switch.tsx
... | ... | @@ -0,0 +1,80 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useSyncExternalStore } from "react"; | |
| 4 | +import { | |
| 5 | + DEFAULT_THEME_PREFERENCE, | |
| 6 | + THEME_ATTRIBUTE_NAME, | |
| 7 | + THEME_PREFERENCE_CYCLE, | |
| 8 | + THEME_STORAGE_KEY, | |
| 9 | + type ThemePreference, | |
| 10 | +} from "@/lib/constants/theme"; | |
| 11 | +import styles from "../design.module.scss"; | |
| 12 | + | |
| 13 | +const LABELS: Record<ThemePreference, string> = { | |
| 14 | + system: "시스템", | |
| 15 | + light: "라이트", | |
| 16 | + dark: "다크", | |
| 17 | +}; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * `<html data-theme>` 자체가 SSOT이므로 React state를 두지 않고 DOM을 직접 구독한다 | |
| 21 | + * (앱 헤더의 테마 토글과 동일한 원칙 — 서버/클라이언트 출력이 갈리지 않는다). | |
| 22 | + * 서버 스냅샷은 항상 기본값(system)이라 첫 렌더가 결정적이다. | |
| 23 | + */ | |
| 24 | +function subscribe(onChange: () => void): () => void { | |
| 25 | + const observer = new MutationObserver(onChange); | |
| 26 | + observer.observe(document.documentElement, { | |
| 27 | + attributes: true, | |
| 28 | + attributeFilter: [THEME_ATTRIBUTE_NAME], | |
| 29 | + }); | |
| 30 | + return () => observer.disconnect(); | |
| 31 | +} | |
| 32 | + | |
| 33 | +function getSnapshot(): ThemePreference { | |
| 34 | + const value = document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME); | |
| 35 | + return value === "light" || value === "dark" ? value : DEFAULT_THEME_PREFERENCE; | |
| 36 | +} | |
| 37 | + | |
| 38 | +function getServerSnapshot(): ThemePreference { | |
| 39 | + return DEFAULT_THEME_PREFERENCE; | |
| 40 | +} | |
| 41 | + | |
| 42 | +function applyTheme(next: ThemePreference): void { | |
| 43 | + const root = document.documentElement; | |
| 44 | + | |
| 45 | + // 전환 트랜지션은 200ms 동안만 켜지는 opt-in이다(@fox/styles/_root.scss). | |
| 46 | + root.setAttribute("data-theme-transition", ""); | |
| 47 | + | |
| 48 | + if (next === "system") { | |
| 49 | + root.removeAttribute(THEME_ATTRIBUTE_NAME); | |
| 50 | + localStorage.removeItem(THEME_STORAGE_KEY); | |
| 51 | + } else { | |
| 52 | + root.setAttribute(THEME_ATTRIBUTE_NAME, next); | |
| 53 | + localStorage.setItem(THEME_STORAGE_KEY, next); | |
| 54 | + } | |
| 55 | + | |
| 56 | + window.setTimeout(() => { | |
| 57 | + root.removeAttribute("data-theme-transition"); | |
| 58 | + }, 200); | |
| 59 | +} | |
| 60 | + | |
| 61 | +/** 카탈로그에서 라이트/다크 토큰을 눈으로 대조하기 위한 개발용 전환기. */ | |
| 62 | +export function ThemeSwitch() { | |
| 63 | + const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); | |
| 64 | + | |
| 65 | + return ( | |
| 66 | + <div className={styles.themeSwitch} role="group" aria-label="테마 전환"> | |
| 67 | + {THEME_PREFERENCE_CYCLE.map((preference) => ( | |
| 68 | + <button | |
| 69 | + key={preference} | |
| 70 | + type="button" | |
| 71 | + className={styles.themeButton} | |
| 72 | + aria-pressed={current === preference} | |
| 73 | + onClick={() => applyTheme(preference)} | |
| 74 | + > | |
| 75 | + {LABELS[preference]} | |
| 76 | + </button> | |
| 77 | + ))} | |
| 78 | + </div> | |
| 79 | + ); | |
| 80 | +} |
+++ app/design/_components/token-catalog.tsx
... | ... | @@ -0,0 +1,187 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { CSSProperties } from "react"; | |
| 4 | +import { useTokenRegistry } from "../_hooks/use-token-registry"; | |
| 5 | +import { typoPresetNames, type TokenEntry } from "../_lib/read-tokens"; | |
| 6 | +import { ThemeSwitch } from "./theme-switch"; | |
| 7 | +import styles from "../design.module.scss"; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * ⚠️ 이 파일은 인라인 `style`을 쓰는 **유일하게 허용되는 예외**다. 카탈로그는 토큰 | |
| 11 | + * 레지스트리를 런타임에 순회하며 개수를 미리 알 수 없는 토큰을 렌더해야 하는데, CSS | |
| 12 | + * 클래스는 알 수 없는 이름에 대해 만들 수 없다. 여기서 인라인으로 넣는 값은 디자인 | |
| 13 | + * 값이 아니라 **토큰 참조**(`var(--fox-*)`)이며 실제 값은 여전히 토큰이 소유한다. | |
| 14 | + * 일반 화면·컴포넌트에서는 이 패턴을 쓰지 않는다. | |
| 15 | + */ | |
| 16 | +function ref(property: keyof CSSProperties, token: TokenEntry): CSSProperties { | |
| 17 | + return { [property]: `var(${token.property})` } as CSSProperties; | |
| 18 | +} | |
| 19 | + | |
| 20 | +function typoRef(preset: string): CSSProperties { | |
| 21 | + return { | |
| 22 | + fontSize: `var(--fox-typo-${preset}-size)`, | |
| 23 | + fontWeight: `var(--fox-typo-${preset}-weight)`, | |
| 24 | + lineHeight: `var(--fox-typo-${preset}-line-height)`, | |
| 25 | + letterSpacing: `var(--fox-typo-${preset}-letter-spacing)`, | |
| 26 | + }; | |
| 27 | +} | |
| 28 | + | |
| 29 | +function ValueTable({ rows }: { rows: { label: string; value: string }[] }) { | |
| 30 | + return ( | |
| 31 | + <table className={styles.table}> | |
| 32 | + <thead> | |
| 33 | + <tr> | |
| 34 | + <th scope="col">토큰</th> | |
| 35 | + <th scope="col">값</th> | |
| 36 | + </tr> | |
| 37 | + </thead> | |
| 38 | + <tbody> | |
| 39 | + {rows.map((row) => ( | |
| 40 | + <tr key={row.label}> | |
| 41 | + <td>{row.label}</td> | |
| 42 | + <td>{row.value}</td> | |
| 43 | + </tr> | |
| 44 | + ))} | |
| 45 | + </tbody> | |
| 46 | + </table> | |
| 47 | + ); | |
| 48 | +} | |
| 49 | + | |
| 50 | +function rows(group: TokenEntry[] | undefined, fn: string) { | |
| 51 | + return (group ?? []).map((token) => ({ | |
| 52 | + label: `fox.${fn}(${token.name})`, | |
| 53 | + value: token.value, | |
| 54 | + })); | |
| 55 | +} | |
| 56 | + | |
| 57 | +export function TokenCatalog() { | |
| 58 | + // CSSOM은 브라우저에만 있으므로 하이드레이션 이후에 채워진다. 서버 렌더 결과가 | |
| 59 | + // 비어 있어도 개발 전용 페이지라 문제되지 않는다. | |
| 60 | + const registry = useTokenRegistry(); | |
| 61 | + | |
| 62 | + if (!registry) { | |
| 63 | + return ( | |
| 64 | + <main className={styles.page}> | |
| 65 | + <p className={styles.lede}>토큰을 읽는 중…</p> | |
| 66 | + </main> | |
| 67 | + ); | |
| 68 | + } | |
| 69 | + | |
| 70 | + const colors = registry.color ?? []; | |
| 71 | + const spaces = registry.space ?? []; | |
| 72 | + const radii = registry.radius ?? []; | |
| 73 | + const shadows = registry.shadow ?? []; | |
| 74 | + const presets = typoPresetNames(registry); | |
| 75 | + | |
| 76 | + return ( | |
| 77 | + <main className={styles.page}> | |
| 78 | + <header className={styles.header}> | |
| 79 | + <div> | |
| 80 | + <h1 className={styles.title}>디자인 시스템 카탈로그</h1> | |
| 81 | + <p className={styles.lede}> | |
| 82 | + @fox/styles/tokens 전량을 CSSOM에서 읽어 그립니다. 개발 환경에서만 열립니다. | |
| 83 | + </p> | |
| 84 | + </div> | |
| 85 | + <ThemeSwitch /> | |
| 86 | + </header> | |
| 87 | + | |
| 88 | + <section className={styles.section}> | |
| 89 | + <h2 className={styles.sectionTitle}>색상 · {colors.length}</h2> | |
| 90 | + <p className={styles.sectionNote}> | |
| 91 | + 라이트/다크 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를 | |
| 92 | + 대조해 보세요. | |
| 93 | + </p> | |
| 94 | + <div className={styles.grid}> | |
| 95 | + {colors.map((token) => ( | |
| 96 | + <div key={token.property} className={styles.swatch}> | |
| 97 | + <div className={styles.swatchChip} style={ref("background", token)} /> | |
| 98 | + <div className={styles.swatchMeta}> | |
| 99 | + <span className={styles.name}>fox.color({token.name})</span> | |
| 100 | + <span className={styles.ref}>{token.value}</span> | |
| 101 | + </div> | |
| 102 | + </div> | |
| 103 | + ))} | |
| 104 | + </div> | |
| 105 | + </section> | |
| 106 | + | |
| 107 | + <section className={styles.section}> | |
| 108 | + <h2 className={styles.sectionTitle}>타이포 프리셋 · {presets.length}</h2> | |
| 109 | + <p className={styles.sectionNote}> | |
| 110 | + 크기·굵기·행간·자간 4종이 한 묶음입니다. @include fox.typo(이름)으로 적용합니다. | |
| 111 | + </p> | |
| 112 | + {presets.map((preset) => ( | |
| 113 | + <div key={preset} className={styles.typoRow}> | |
| 114 | + <span className={styles.typoName}>{preset}</span> | |
| 115 | + <span className={styles.typoSample} style={typoRef(preset)}> | |
| 116 | + 다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123 | |
| 117 | + </span> | |
| 118 | + </div> | |
| 119 | + ))} | |
| 120 | + </section> | |
| 121 | + | |
| 122 | + <section className={styles.section}> | |
| 123 | + <h2 className={styles.sectionTitle}>간격 · {spaces.length}</h2> | |
| 124 | + <p className={styles.sectionNote}> | |
| 125 | + 4px 배수 스케일. 1rem = 10px이므로 fox.space(4) = 1.6rem = 16px입니다. | |
| 126 | + </p> | |
| 127 | + {spaces.map((token) => ( | |
| 128 | + <div key={token.property} className={styles.spaceRow}> | |
| 129 | + <span className={styles.typoName}> | |
| 130 | + space({token.name}) · {token.value} | |
| 131 | + </span> | |
| 132 | + <span className={styles.spaceBar} style={ref("inlineSize", token)} /> | |
| 133 | + </div> | |
| 134 | + ))} | |
| 135 | + </section> | |
| 136 | + | |
| 137 | + <section className={styles.section}> | |
| 138 | + <h2 className={styles.sectionTitle}>모서리 반경 · {radii.length}</h2> | |
| 139 | + <div className={styles.grid}> | |
| 140 | + {radii.map((token) => ( | |
| 141 | + <div key={token.property} className={styles.tile} style={ref("borderRadius", token)}> | |
| 142 | + {token.name} · {token.value} | |
| 143 | + </div> | |
| 144 | + ))} | |
| 145 | + </div> | |
| 146 | + </section> | |
| 147 | + | |
| 148 | + <section className={styles.section}> | |
| 149 | + <h2 className={styles.sectionTitle}>그림자 · {shadows.length}</h2> | |
| 150 | + <p className={styles.sectionNote}>크기가 아니라 쓰임(고도)으로 이름 붙였습니다.</p> | |
| 151 | + <div className={styles.grid}> | |
| 152 | + {shadows.map((token) => ( | |
| 153 | + <div key={token.property} className={styles.shadowCell}> | |
| 154 | + <div className={styles.shadowTile} style={ref("boxShadow", token)}> | |
| 155 | + {token.name} | |
| 156 | + </div> | |
| 157 | + </div> | |
| 158 | + ))} | |
| 159 | + </div> | |
| 160 | + </section> | |
| 161 | + | |
| 162 | + <section className={styles.section}> | |
| 163 | + <h2 className={styles.sectionTitle}>모션</h2> | |
| 164 | + <ValueTable | |
| 165 | + rows={[...rows(registry.duration, "duration"), ...rows(registry.easing, "easing")]} | |
| 166 | + /> | |
| 167 | + </section> | |
| 168 | + | |
| 169 | + <section className={styles.section}> | |
| 170 | + <h2 className={styles.sectionTitle}>서체 · 굵기 · z 층 · 브레이크포인트</h2> | |
| 171 | + <p className={styles.sectionNote}> | |
| 172 | + 브레이크포인트는 미디어 쿼리가 var()를 해석하지 못하므로 실제 사용은 fox.media() | |
| 173 | + 믹스인 경유입니다. 또한 미디어 쿼리 안의 rem은 html font-size의 영향을 받지 않아 | |
| 174 | + 48rem은 480px가 아니라 768px입니다. | |
| 175 | + </p> | |
| 176 | + <ValueTable | |
| 177 | + rows={[ | |
| 178 | + ...rows(registry.font, "font"), | |
| 179 | + ...rows(registry.weight, "weight"), | |
| 180 | + ...rows(registry.z, "z"), | |
| 181 | + ...rows(registry.breakpoint, "media"), | |
| 182 | + ]} | |
| 183 | + /> | |
| 184 | + </section> | |
| 185 | + </main> | |
| 186 | + ); | |
| 187 | +} |
+++ app/design/_hooks/use-token-registry.ts
... | ... | @@ -0,0 +1,45 @@ |
| 1 | +import { useSyncExternalStore } from "react"; | |
| 2 | +import { readTokenRegistry, type TokenRegistry } from "../_lib/read-tokens"; | |
| 3 | +import { THEME_ATTRIBUTE_NAME } from "@/lib/constants/theme"; | |
| 4 | + | |
| 5 | +// CSSOM은 React 바깥의 외부 시스템이라 `useSyncExternalStore`가 맞는 도구다 | |
| 6 | +// (effect에서 setState 하면 렌더가 연쇄된다). | |
| 7 | +// | |
| 8 | +// 토큰 **값**은 현재 테마에 따라 달라지므로 `data-theme`이 바뀌면 다시 읽어야 한다. | |
| 9 | +// 반면 `useSyncExternalStore`는 매 렌더마다 getSnapshot을 호출하므로 매번 새 객체를 | |
| 10 | +// 돌려주면 무한 루프가 된다 — 그래서 테마 키로 캐시해 같은 테마 동안에는 동일한 | |
| 11 | +// 참조를 반환한다. | |
| 12 | +let cached: { theme: string; registry: TokenRegistry } | null = null; | |
| 13 | + | |
| 14 | +function currentTheme(): string { | |
| 15 | + return document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME) ?? "system"; | |
| 16 | +} | |
| 17 | + | |
| 18 | +function subscribe(onChange: () => void): () => void { | |
| 19 | + const observer = new MutationObserver(onChange); | |
| 20 | + observer.observe(document.documentElement, { | |
| 21 | + attributes: true, | |
| 22 | + attributeFilter: [THEME_ATTRIBUTE_NAME], | |
| 23 | + }); | |
| 24 | + return () => observer.disconnect(); | |
| 25 | +} | |
| 26 | + | |
| 27 | +function getSnapshot(): TokenRegistry | null { | |
| 28 | + const theme = currentTheme(); | |
| 29 | + if (!cached || cached.theme !== theme) { | |
| 30 | + cached = { theme, registry: readTokenRegistry() }; | |
| 31 | + } | |
| 32 | + return cached.registry; | |
| 33 | +} | |
| 34 | + | |
| 35 | +function getServerSnapshot(): TokenRegistry | null { | |
| 36 | + return null; | |
| 37 | +} | |
| 38 | + | |
| 39 | +/** | |
| 40 | + * 서버 렌더와 하이드레이션 첫 렌더에서는 `null`, 그 이후 토큰 레지스트리를 반환한다. | |
| 41 | + * 테마를 바꾸면 그 테마에서 해석된 값으로 갱신된다. | |
| 42 | + */ | |
| 43 | +export function useTokenRegistry(): TokenRegistry | null { | |
| 44 | + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); | |
| 45 | +} |
+++ app/design/_lib/read-tokens.ts
... | ... | @@ -0,0 +1,99 @@ |
| 1 | +export interface TokenEntry { | |
| 2 | + /** 토큰 이름 (`--fox-color-primary` → `primary`) */ | |
| 3 | + name: string; | |
| 4 | + /** | |
| 5 | + * 현재 테마에서 실제로 해석된 값 (`#818cf8`). | |
| 6 | + * | |
| 7 | + * 선언 원문이 아니라 계산값을 쓴다 — Lightning CSS가 `light-dark()`를 | |
| 8 | + * `var(--lightningcss-light, …) var(--lightningcss-dark, …)`로 폴리필해 내보내므로 | |
| 9 | + * 원문은 읽기 어렵고, 계산값은 지금 테마에서 눈에 보이는 색과 정확히 일치한다. | |
| 10 | + */ | |
| 11 | + value: string; | |
| 12 | + /** 스타일에서 참조할 커스텀 프로퍼티 전체 이름 */ | |
| 13 | + property: string; | |
| 14 | +} | |
| 15 | + | |
| 16 | +export type TokenRegistry = Record<string, TokenEntry[]>; | |
| 17 | + | |
| 18 | +const PREFIX = "--fox-"; | |
| 19 | +const TYPO_FIELDS = ["-size", "-weight", "-line-height", "-letter-spacing"]; | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * 문서의 모든 스타일시트를 훑어 `--fox-*` 커스텀 프로퍼티를 그룹별로 모은다. | |
| 23 | + * | |
| 24 | + * 토큰 목록을 카탈로그가 따로 들고 있지 않게 하려는 것이 목적이다 — `tokens/`에 값을 | |
| 25 | + * 추가하면 카탈로그에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다. | |
| 26 | + * CSSOM은 선언 순서를 보존하므로 SCSS map에 적은 순서가 그대로 유지된다. | |
| 27 | + * | |
| 28 | + * 브라우저에서만 동작한다(문서가 있어야 한다). | |
| 29 | + */ | |
| 30 | +export function readTokenRegistry(): TokenRegistry { | |
| 31 | + const registry: TokenRegistry = {}; | |
| 32 | + const seen = new Set<string>(); | |
| 33 | + // 이름·순서는 CSSOM 규칙에서, 값은 계산 스타일에서 가져온다. | |
| 34 | + const computed = getComputedStyle(document.documentElement); | |
| 35 | + | |
| 36 | + for (const sheet of Array.from(document.styleSheets)) { | |
| 37 | + let rules: CSSRuleList; | |
| 38 | + try { | |
| 39 | + rules = sheet.cssRules; | |
| 40 | + } catch { | |
| 41 | + // 교차 출처 스타일시트는 규칙을 읽을 수 없다 — 우리 토큰은 동일 출처이므로 건너뛴다. | |
| 42 | + continue; | |
| 43 | + } | |
| 44 | + | |
| 45 | + for (const rule of Array.from(rules)) { | |
| 46 | + if (!(rule instanceof CSSStyleRule) || rule.selectorText !== ":root") { | |
| 47 | + continue; | |
| 48 | + } | |
| 49 | + | |
| 50 | + for (const property of Array.from(rule.style)) { | |
| 51 | + if (!property.startsWith(PREFIX) || seen.has(property)) { | |
| 52 | + continue; | |
| 53 | + } | |
| 54 | + seen.add(property); | |
| 55 | + | |
| 56 | + const rest = property.slice(PREFIX.length); | |
| 57 | + const separator = rest.indexOf("-"); | |
| 58 | + if (separator <= 0) { | |
| 59 | + continue; | |
| 60 | + } | |
| 61 | + | |
| 62 | + const group = rest.slice(0, separator); | |
| 63 | + const name = rest.slice(separator + 1); | |
| 64 | + | |
| 65 | + (registry[group] ??= []).push({ | |
| 66 | + name, | |
| 67 | + value: | |
| 68 | + computed.getPropertyValue(property).trim() || | |
| 69 | + rule.style.getPropertyValue(property).trim(), | |
| 70 | + property, | |
| 71 | + }); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + } | |
| 75 | + | |
| 76 | + return registry; | |
| 77 | +} | |
| 78 | + | |
| 79 | +/** | |
| 80 | + * 타이포 프리셋은 이름 하나가 4개의 커스텀 프로퍼티로 펼쳐져 있으므로 | |
| 81 | + * (`--fox-typo-title-md-size` 등) 접미사를 떼어 프리셋 이름만 순서대로 복원한다. | |
| 82 | + */ | |
| 83 | +export function typoPresetNames(registry: TokenRegistry): string[] { | |
| 84 | + const names: string[] = []; | |
| 85 | + | |
| 86 | + for (const entry of registry.typo ?? []) { | |
| 87 | + const field = TYPO_FIELDS.find((suffix) => entry.name.endsWith(suffix)); | |
| 88 | + if (!field) { | |
| 89 | + continue; | |
| 90 | + } | |
| 91 | + | |
| 92 | + const preset = entry.name.slice(0, -field.length); | |
| 93 | + if (!names.includes(preset)) { | |
| 94 | + names.push(preset); | |
| 95 | + } | |
| 96 | + } | |
| 97 | + | |
| 98 | + return names; | |
| 99 | +} |
+++ app/design/design.module.scss
... | ... | @@ -0,0 +1,211 @@ |
| 1 | +// 디자인 시스템 카탈로그 스타일. | |
| 2 | +// | |
| 3 | +// 토큰 목록은 이 파일이 아니라 **CSSOM에서 런타임에 읽는다**(`_components/token-catalog.tsx`). | |
| 4 | +// SCSS 값을 JS로 내보내는 CSS Modules의 `:export`를 처음 썼으나, Next.js 16의 기본 | |
| 5 | +// 번들러인 Turbopack은 Lightning CSS로 CSS를 파싱해 `:export`(webpack css-loader 시절의 | |
| 6 | +// ICSS 확장)를 인식하지 못한다 — 빌드는 통과하지만 규칙이 통째로 버려져 값이 `undefined`가 | |
| 7 | +// 된다. `--fox-*` 커스텀 프로퍼티를 CSSOM에서 직접 열거하면 같은 "목록을 두 곳에 두지 | |
| 8 | +// 않는다"는 목적을 번들러 의존 없이 달성한다. | |
| 9 | + | |
| 10 | +@use "@fox/styles/abstracts" as fox; | |
| 11 | + | |
| 12 | +.page { | |
| 13 | + flex: 1; | |
| 14 | + padding: fox.space(8) fox.space(6); | |
| 15 | + | |
| 16 | + @include fox.container(xl, 6); | |
| 17 | +} | |
| 18 | + | |
| 19 | +.header { | |
| 20 | + display: flex; | |
| 21 | + flex-wrap: wrap; | |
| 22 | + gap: fox.space(4); | |
| 23 | + align-items: baseline; | |
| 24 | + justify-content: space-between; | |
| 25 | + padding-block-end: fox.space(6); | |
| 26 | + border-block-end: 1px solid fox.color(border); | |
| 27 | +} | |
| 28 | + | |
| 29 | +.title { | |
| 30 | + @include fox.typo(headline-lg); | |
| 31 | +} | |
| 32 | + | |
| 33 | +.lede { | |
| 34 | + margin-block-start: fox.space(2); | |
| 35 | + color: fox.color(foreground-muted); | |
| 36 | + | |
| 37 | + @include fox.typo(body-md); | |
| 38 | +} | |
| 39 | + | |
| 40 | +// ── 테마 전환 ─────────────────────────────────────────────────────────────── | |
| 41 | +.themeSwitch { | |
| 42 | + display: flex; | |
| 43 | + gap: fox.space(1); | |
| 44 | + padding: fox.space(1); | |
| 45 | + border: 1px solid fox.color(border); | |
| 46 | + border-radius: fox.radius(full); | |
| 47 | + background: fox.color(surface-muted); | |
| 48 | +} | |
| 49 | + | |
| 50 | +.themeButton { | |
| 51 | + padding: fox.space(2) fox.space(4); | |
| 52 | + border-radius: fox.radius(full); | |
| 53 | + color: fox.color(foreground-muted); | |
| 54 | + | |
| 55 | + @include fox.typo(label-md); | |
| 56 | + @include fox.focus-ring; | |
| 57 | + @include fox.transition((background-color, color)); | |
| 58 | + | |
| 59 | + &[aria-pressed="true"] { | |
| 60 | + background: fox.color(primary); | |
| 61 | + color: fox.color(on-primary); | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +.section { | |
| 66 | + padding-block: fox.space(8); | |
| 67 | + border-block-end: 1px solid fox.color(border-subtle); | |
| 68 | +} | |
| 69 | + | |
| 70 | +.sectionTitle { | |
| 71 | + margin-block-end: fox.space(1); | |
| 72 | + | |
| 73 | + @include fox.typo(title-lg); | |
| 74 | +} | |
| 75 | + | |
| 76 | +.sectionNote { | |
| 77 | + margin-block-end: fox.space(5); | |
| 78 | + color: fox.color(foreground-muted); | |
| 79 | + | |
| 80 | + @include fox.typo(body-sm); | |
| 81 | +} | |
| 82 | + | |
| 83 | +.grid { | |
| 84 | + display: grid; | |
| 85 | + grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); | |
| 86 | + gap: fox.space(4); | |
| 87 | +} | |
| 88 | + | |
| 89 | +// ── 색상 ──────────────────────────────────────────────────────────────────── | |
| 90 | +.swatch { | |
| 91 | + overflow: hidden; | |
| 92 | + border: 1px solid fox.color(border); | |
| 93 | + border-radius: fox.radius(md); | |
| 94 | + background: fox.color(surface); | |
| 95 | +} | |
| 96 | + | |
| 97 | +.swatchChip { | |
| 98 | + block-size: 6.4rem; | |
| 99 | +} | |
| 100 | + | |
| 101 | +.swatchMeta { | |
| 102 | + padding: fox.space(3); | |
| 103 | +} | |
| 104 | + | |
| 105 | +.name { | |
| 106 | + display: block; | |
| 107 | + color: fox.color(foreground); | |
| 108 | + word-break: break-all; | |
| 109 | + | |
| 110 | + @include fox.typo(label-lg); | |
| 111 | +} | |
| 112 | + | |
| 113 | +.ref { | |
| 114 | + display: block; | |
| 115 | + margin-block-start: fox.space(1); | |
| 116 | + color: fox.color(foreground-subtle); | |
| 117 | + font-family: fox.font(mono); | |
| 118 | + word-break: break-all; | |
| 119 | + | |
| 120 | + @include fox.typo(body-sm); | |
| 121 | +} | |
| 122 | + | |
| 123 | +// ── 타이포 ────────────────────────────────────────────────────────────────── | |
| 124 | +.typoRow { | |
| 125 | + display: flex; | |
| 126 | + flex-wrap: wrap; | |
| 127 | + gap: fox.space(2) fox.space(6); | |
| 128 | + align-items: baseline; | |
| 129 | + padding-block: fox.space(3); | |
| 130 | + border-block-end: 1px solid fox.color(border-subtle); | |
| 131 | +} | |
| 132 | + | |
| 133 | +.typoName { | |
| 134 | + flex: 0 0 14rem; | |
| 135 | + color: fox.color(foreground-subtle); | |
| 136 | + font-family: fox.font(mono); | |
| 137 | + | |
| 138 | + @include fox.typo(body-sm); | |
| 139 | +} | |
| 140 | + | |
| 141 | +.typoSample { | |
| 142 | + flex: 1; | |
| 143 | + min-inline-size: 20rem; | |
| 144 | +} | |
| 145 | + | |
| 146 | +// ── 간격 ──────────────────────────────────────────────────────────────────── | |
| 147 | +.spaceRow { | |
| 148 | + display: flex; | |
| 149 | + gap: fox.space(4); | |
| 150 | + align-items: center; | |
| 151 | + padding-block: fox.space(2); | |
| 152 | +} | |
| 153 | + | |
| 154 | +.spaceBar { | |
| 155 | + block-size: 1.6rem; | |
| 156 | + border-radius: fox.radius(xs); | |
| 157 | + background: fox.color(primary); | |
| 158 | +} | |
| 159 | + | |
| 160 | +// ── radius · shadow ───────────────────────────────────────────────────────── | |
| 161 | +.tile { | |
| 162 | + display: flex; | |
| 163 | + block-size: 9.6rem; | |
| 164 | + align-items: center; | |
| 165 | + justify-content: center; | |
| 166 | + border: 1px solid fox.color(border); | |
| 167 | + background: fox.color(surface); | |
| 168 | + color: fox.color(foreground-muted); | |
| 169 | + | |
| 170 | + @include fox.typo(label-md); | |
| 171 | +} | |
| 172 | + | |
| 173 | +.shadowTile { | |
| 174 | + display: flex; | |
| 175 | + block-size: 9.6rem; | |
| 176 | + align-items: center; | |
| 177 | + justify-content: center; | |
| 178 | + border-radius: fox.radius(lg); | |
| 179 | + background: fox.color(surface); | |
| 180 | + color: fox.color(foreground-muted); | |
| 181 | + | |
| 182 | + @include fox.typo(label-md); | |
| 183 | +} | |
| 184 | + | |
| 185 | +.shadowCell { | |
| 186 | + padding: fox.space(4); | |
| 187 | +} | |
| 188 | + | |
| 189 | +// ── 값 테이블 ─────────────────────────────────────────────────────────────── | |
| 190 | +.table { | |
| 191 | + inline-size: 100%; | |
| 192 | + text-align: start; | |
| 193 | +} | |
| 194 | + | |
| 195 | +.table th, | |
| 196 | +.table td { | |
| 197 | + padding: fox.space(2) fox.space(3); | |
| 198 | + border-block-end: 1px solid fox.color(border-subtle); | |
| 199 | +} | |
| 200 | + | |
| 201 | +.table th { | |
| 202 | + color: fox.color(foreground-muted); | |
| 203 | + | |
| 204 | + @include fox.typo(label-md); | |
| 205 | +} | |
| 206 | + | |
| 207 | +.table td { | |
| 208 | + font-family: fox.font(mono); | |
| 209 | + | |
| 210 | + @include fox.typo(body-sm); | |
| 211 | +} |
+++ app/design/page.tsx
... | ... | @@ -0,0 +1,20 @@ |
| 1 | +import { notFound } from "next/navigation"; | |
| 2 | +import { TokenCatalog } from "./_components/token-catalog"; | |
| 3 | + | |
| 4 | +export const metadata = { | |
| 5 | + title: "디자인 시스템 카탈로그", | |
| 6 | +}; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 디자인 시스템 토큰 카탈로그 — 개발 전용. | |
| 10 | + * | |
| 11 | + * 라우트 자체는 서버에 남기고(프로덕션 차단을 서버에서 판정), 실제 렌더는 CSSOM을 읽어야 | |
| 12 | + * 하는 클라이언트 컴포넌트가 맡는다. | |
| 13 | + */ | |
| 14 | +export default function DesignCatalogPage() { | |
| 15 | + if (process.env.NODE_ENV === "production") { | |
| 16 | + notFound(); | |
| 17 | + } | |
| 18 | + | |
| 19 | + return <TokenCatalog />; | |
| 20 | +} |
--- app/globals.css
... | ... | @@ -1,273 +0,0 @@ |
| 1 | -@import "tailwindcss"; | |
| 2 | - | |
| 3 | -/* 색상 토큰 출처: edupay-app lib/ui/core/tokens/app_colors.dart | |
| 4 | - (Figma: FOX PAY 1차 제작시안, node 91:10205). 다크 값은 edupay-app에 대응 | |
| 5 | - 다크 시안이 없어 "임의 도출 — 정식 다크 시안 확정 시 교체". | |
| 6 | - 메커니즘: `color-scheme` + `light-dark()`(Chrome 123+/Safari 17.5+/Firefox 120+ | |
| 7 | - 지원, 미지원 브라우저는 값이 무효화되므로 사내 어드민 한정 트레이드오프로 채택 | |
| 8 | - — design 계획 §4.1/§5.3, 2026-07-28 게이트 승인). 각 시맨틱 토큰이 | |
| 9 | - `light-dark(라이트값, 다크값)`으로 라이트/다크 값을 한 선언에 함께 보유해 | |
| 10 | - 드리프트를 구조적으로 차단한다. 수동 전환은 아래 `:root[data-theme]`가 | |
| 11 | - `color-scheme`만 덮어써 처리하므로 값 자체는 이 블록 한 곳에만 존재한다 | |
| 12 | - (Next.js 16 번들 문서 preventing-flash-before-hydration.md §Themes 패턴). */ | |
| 13 | -:root { | |
| 14 | - color-scheme: light dark; | |
| 15 | - | |
| 16 | - /* Brand */ | |
| 17 | - --primary: light-dark(#502ef5, #877aec); | |
| 18 | - --primary-container: light-dark(#6d5ce7, #502ef5); | |
| 19 | - /* on-primary 다크값 대비 보정: 원래 #ffffff였으나 다크 --primary(#877aec) 위에서 | |
| 20 | - 3.49:1로 WCAG AA(4.5:1) 텍스트 기준 미달 확인 → #14161b로 교체해 5.18:1 통과. | |
| 21 | - Material 3 다크 규약(밝은 primary 표면 + 어두운 onPrimary 텍스트)과 일치하며 | |
| 22 | - text-primary on surface(4.61:1)에는 영향 없음 — design 계획 §5.1(i), | |
| 23 | - 2026-07-28 게이트 승인. */ | |
| 24 | - --on-primary: light-dark(#ffffff, #14161b); | |
| 25 | - --on-primary-container: light-dark(#e2e0fb, #e2e0fb); | |
| 26 | - --secondary: light-dark(#877aec, #9b92ff); | |
| 27 | - | |
| 28 | - /* Surface / background (원 토큰: surface / gray0) */ | |
| 29 | - --background: light-dark(#ffffff, #14161b); | |
| 30 | - --surface: light-dark(#ffffff, #1e2128); | |
| 31 | - --surface-muted: light-dark(#f5f7fa, #2c3038); | |
| 32 | - | |
| 33 | - /* Foreground / text (원 토큰: onSurface 계열) */ | |
| 34 | - --foreground: light-dark(#2c3038, #f5f7fa); | |
| 35 | - --foreground-muted: light-dark(#565d6b, #bbc4d8); | |
| 36 | - --foreground-subtle: light-dark(#6b7486, #8790a3); | |
| 37 | - | |
| 38 | - /* Border (원 토큰: outline 계열) */ | |
| 39 | - --border: light-dark(#dae0ed, #424752); | |
| 40 | - --border-subtle: light-dark(#e1e6f1, #343943); | |
| 41 | - | |
| 42 | - /* Status */ | |
| 43 | - --danger: light-dark(#ec2e2f, #ff5c5d); | |
| 44 | - --success: light-dark(#21b770, #3acb93); | |
| 45 | - --warning: light-dark(#ffb020, #ffc24d); | |
| 46 | - | |
| 47 | - /* Overlay */ | |
| 48 | - --scrim: light-dark(rgb(0 0 0 / 0.6), rgb(0 0 0 / 0.7)); | |
| 49 | -} | |
| 50 | - | |
| 51 | -/* 수동 테마 선택 — <html data-theme="light"|"dark">가 `color-scheme`만 덮어써 | |
| 52 | - 위 light-dark() 값이 해당 분기로 고정된다(값 자체는 여기서 재정의하지 않음). | |
| 53 | - data-theme 속성이 없거나 "system"이면 :root의 `color-scheme: light dark`가 | |
| 54 | - 유지되어 OS `prefers-color-scheme`를 그대로 따른다(feature 전달 사양). */ | |
| 55 | -:root[data-theme="light"] { | |
| 56 | - color-scheme: light; | |
| 57 | -} | |
| 58 | - | |
| 59 | -:root[data-theme="dark"] { | |
| 60 | - color-scheme: dark; | |
| 61 | -} | |
| 62 | - | |
| 63 | -@theme inline { | |
| 64 | - --color-primary: var(--primary); | |
| 65 | - --color-primary-container: var(--primary-container); | |
| 66 | - --color-on-primary: var(--on-primary); | |
| 67 | - --color-on-primary-container: var(--on-primary-container); | |
| 68 | - --color-secondary: var(--secondary); | |
| 69 | - | |
| 70 | - --color-background: var(--background); | |
| 71 | - --color-surface: var(--surface); | |
| 72 | - --color-surface-muted: var(--surface-muted); | |
| 73 | - | |
| 74 | - --color-foreground: var(--foreground); | |
| 75 | - --color-foreground-muted: var(--foreground-muted); | |
| 76 | - --color-foreground-subtle: var(--foreground-subtle); | |
| 77 | - | |
| 78 | - --color-border: var(--border); | |
| 79 | - --color-border-subtle: var(--border-subtle); | |
| 80 | - | |
| 81 | - --color-danger: var(--danger); | |
| 82 | - --color-success: var(--success); | |
| 83 | - --color-warning: var(--warning); | |
| 84 | - | |
| 85 | - --color-scrim: var(--scrim); | |
| 86 | - | |
| 87 | - /* WantedSans 연결 전에도 안전한 폴백(feature 레인이 app/layout.tsx에서 | |
| 88 | - next/font/local로 --font-wanted-sans를 정의). */ | |
| 89 | - --font-sans: var(--font-wanted-sans, system-ui), system-ui, | |
| 90 | - "Apple SD Gothic Neo", sans-serif; | |
| 91 | - --font-mono: var(--font-geist-mono); | |
| 92 | - | |
| 93 | - /* 타이포 토큰 출처: edupay-app lib/ui/core/tokens/app_typography.dart | |
| 94 | - (Figma: FOX PAY 1차 제작시안, node 112:11439). voucher/numeric 등 앱 전용 | |
| 95 | - 스타일은 어드민 무관으로 미이식. */ | |
| 96 | - --text-display-lg: 3rem; | |
| 97 | - --text-display-lg--line-height: 1.5; | |
| 98 | - --text-display-lg--font-weight: 700; | |
| 99 | - | |
| 100 | - --text-display-md: 2.25rem; | |
| 101 | - --text-display-md--line-height: 1.5; | |
| 102 | - --text-display-md--letter-spacing: -0.72px; | |
| 103 | - --text-display-md--font-weight: 700; | |
| 104 | - | |
| 105 | - --text-display-sm: 2rem; | |
| 106 | - --text-display-sm--line-height: 1.5; | |
| 107 | - --text-display-sm--letter-spacing: -0.64px; | |
| 108 | - --text-display-sm--font-weight: 700; | |
| 109 | - | |
| 110 | - --text-headline-lg: 1.5rem; | |
| 111 | - --text-headline-lg--line-height: 1.5; | |
| 112 | - --text-headline-lg--letter-spacing: -0.72px; | |
| 113 | - --text-headline-lg--font-weight: 700; | |
| 114 | - | |
| 115 | - --text-headline-md: 1.25rem; | |
| 116 | - --text-headline-md--line-height: 1.5; | |
| 117 | - --text-headline-md--letter-spacing: -0.6px; | |
| 118 | - --text-headline-md--font-weight: 700; | |
| 119 | - | |
| 120 | - --text-headline-sm: 1.125rem; | |
| 121 | - --text-headline-sm--line-height: 1.3; | |
| 122 | - --text-headline-sm--letter-spacing: -0.54px; | |
| 123 | - --text-headline-sm--font-weight: 700; | |
| 124 | - | |
| 125 | - --text-title-lg: 1.125rem; | |
| 126 | - --text-title-lg--line-height: 1.5; | |
| 127 | - --text-title-lg--font-weight: 700; | |
| 128 | - | |
| 129 | - --text-title-md: 1rem; | |
| 130 | - --text-title-md--line-height: 1.5; | |
| 131 | - --text-title-md--font-weight: 700; | |
| 132 | - | |
| 133 | - --text-title-sm: 0.875rem; | |
| 134 | - --text-title-sm--line-height: 1.5; | |
| 135 | - --text-title-sm--letter-spacing: -0.42px; | |
| 136 | - --text-title-sm--font-weight: 700; | |
| 137 | - | |
| 138 | - --text-body-lg: 1rem; | |
| 139 | - --text-body-lg--line-height: 1.5; | |
| 140 | - --text-body-lg--letter-spacing: -0.48px; | |
| 141 | - --text-body-lg--font-weight: 400; | |
| 142 | - | |
| 143 | - --text-body-md: 0.875rem; | |
| 144 | - --text-body-md--line-height: 1.5; | |
| 145 | - --text-body-md--font-weight: 400; | |
| 146 | - | |
| 147 | - --text-body-sm: 0.75rem; | |
| 148 | - --text-body-sm--line-height: 1.5; | |
| 149 | - --text-body-sm--font-weight: 400; | |
| 150 | - | |
| 151 | - --text-label-lg: 1rem; | |
| 152 | - --text-label-lg--line-height: 1.4; | |
| 153 | - --text-label-lg--letter-spacing: -0.48px; | |
| 154 | - --text-label-lg--font-weight: 700; | |
| 155 | - | |
| 156 | - --text-label-md: 0.875rem; | |
| 157 | - --text-label-md--letter-spacing: -0.28px; | |
| 158 | - --text-label-md--font-weight: 400; | |
| 159 | - | |
| 160 | - --text-label-sm: 0.75rem; | |
| 161 | - --text-label-sm--font-weight: 400; | |
| 162 | - | |
| 163 | - /* radius 토큰 출처: edupay-app lib/ui/core/tokens/app_radius.dart. | |
| 164 | - Tailwind 기본 스케일과 다른 값으로 의도적 재정의(앱 일체화, §8-4 승인). | |
| 165 | - pill(999)은 기본 rounded-full로 충족, card(50)은 앱 전용 미이식. */ | |
| 166 | - --radius-xs: 0.25rem; | |
| 167 | - --radius-sm: 0.5rem; | |
| 168 | - --radius-md: 0.75rem; | |
| 169 | - --radius-lg: 1rem; | |
| 170 | - --radius-xl: 1.25rem; | |
| 171 | - --radius-2xl: 1.5rem; | |
| 172 | - | |
| 173 | - /* 그림자 토큰 출처: edupay-app lib/ui/core/tokens/app_shadows.dart — | |
| 174 | - 어드민 유효분만 이식(voucher inner·stacked 등 미이식). */ | |
| 175 | - --shadow-card: 0 5.16px 5.16px rgb(93 100 119 / 0.12); | |
| 176 | - --shadow-list-card: 0 8px 16px rgb(93 100 119 / 0.12); | |
| 177 | - --shadow-dialog: 0 8px 16px rgb(0 0 0 / 0.1); | |
| 178 | - --shadow-popup: 2.16px 6.32px 6px rgb(177 178 180 / 0.24); | |
| 179 | - --shadow-toast: 0 10px 12px rgb(0 0 0 / 0.2); | |
| 180 | - --shadow-subtle: 0 2px 4px rgb(0 0 0 / 0.08); | |
| 181 | - | |
| 182 | - /* 애니메이션 토큰(신규 네임스페이스 — §4.2 승인). 오버레이·다이얼로그·토스트· | |
| 183 | - 스플래시 전환에 사용. --ease-*는 Tailwind 기본값 사용, duration은 값에 | |
| 184 | - 내장(트랜지션 duration 표준은 bare 유틸 duration-200 사용). */ | |
| 185 | - --animate-fade-in: fade-in 200ms var(--ease-out); | |
| 186 | - --animate-fade-out: fade-out 200ms var(--ease-in); | |
| 187 | - --animate-dialog-in: dialog-in 200ms var(--ease-out); | |
| 188 | - --animate-toast-in: toast-in 300ms var(--ease-out); | |
| 189 | -} | |
| 190 | - | |
| 191 | -@keyframes fade-in { | |
| 192 | - from { | |
| 193 | - opacity: 0; | |
| 194 | - } | |
| 195 | - to { | |
| 196 | - opacity: 1; | |
| 197 | - } | |
| 198 | -} | |
| 199 | - | |
| 200 | -/* 스플래시 등 풀스크린 오버레이 퇴장용 — fade-in의 역방향 키프레임. */ | |
| 201 | -@keyframes fade-out { | |
| 202 | - from { | |
| 203 | - opacity: 1; | |
| 204 | - } | |
| 205 | - to { | |
| 206 | - opacity: 0; | |
| 207 | - } | |
| 208 | -} | |
| 209 | - | |
| 210 | -@keyframes dialog-in { | |
| 211 | - from { | |
| 212 | - opacity: 0; | |
| 213 | - transform: scale(0.95); | |
| 214 | - } | |
| 215 | - to { | |
| 216 | - opacity: 1; | |
| 217 | - transform: scale(1); | |
| 218 | - } | |
| 219 | -} | |
| 220 | - | |
| 221 | -@keyframes toast-in { | |
| 222 | - from { | |
| 223 | - opacity: 0; | |
| 224 | - transform: translateY(0.5rem); | |
| 225 | - } | |
| 226 | - to { | |
| 227 | - opacity: 1; | |
| 228 | - transform: translateY(0); | |
| 229 | - } | |
| 230 | -} | |
| 231 | - | |
| 232 | -/* 간격 토큰 매핑 (변경 없음) — edupay-app lib/ui/core/tokens/app_spacing.dart의 | |
| 233 | - 4/8 grid(4·8·12·16·20·24·32·40)는 Tailwind 기본 --spacing: 0.25rem 스케일과 | |
| 234 | - 정확히 일치한다: xs→p-1, sm→p-2, md→p-3, lg→p-4, xl→p-5, xxl→p-6, | |
| 235 | - xxxl→p-8, xxxxl→p-10. */ | |
| 236 | - | |
| 237 | -/* 테마 수동 전환 — feature 소비처(예: 헤더 테마 토글)를 위한 CSS 구동 변형자. | |
| 238 | - design 계획 §9 게이트 교차 검증에서 feature 안(아이콘 3개를 모두 DOM에 렌더하고 | |
| 239 | - CSS만으로 하나를 표시)을 채택했다 — 서버/클라 출력이 항상 동일해 hydration | |
| 240 | - 불일치 표면적이 0이다. `theme-light`/`theme-dark`는 명시적 선택에만 매칭되고, | |
| 241 | - `theme-system`은 data-theme 부재 **및** 명시적 data-theme="system" 값 둘 다 | |
| 242 | - 커버해야 하므로(둘은 동일하게 동작 — feature 전달 사양) 부정 선택자 두 개로 | |
| 243 | - "light도 dark도 아님"을 표현한다(속성 부재 시에도 :not()은 매칭된다). */ | |
| 244 | -@custom-variant theme-system (&:where(html:not([data-theme="light"]):not([data-theme="dark"]) *)); | |
| 245 | -@custom-variant theme-light (&:where(html[data-theme="light"] *)); | |
| 246 | -@custom-variant theme-dark (&:where(html[data-theme="dark"] *)); | |
| 247 | - | |
| 248 | -/* 테마 전환 색상 트랜지션 — feature가 토글 직전 <html>에 [data-theme-transition]을 | |
| 249 | - 부여하고 ~200ms 후 제거하는 시간 한정 opt-in이다(상시 트랜지션이 아니므로 다른 | |
| 250 | - hover/트랜지션 유틸과 경쟁하지 않는다). prefers-reduced-motion: no-preference로 | |
| 251 | - 게이팅해 감소 모션 환경에서는 아무 트랜지션도 걸리지 않는다(전환 없이 즉시 반영). | |
| 252 | - !important는 유틸리티의 transition 계열 클래스(transition-none 포함)가 이 200ms | |
| 253 | - 동안만 존재하는 규칙을 덮어쓰지 않도록 한다. box-shadow는 제외 — --shadow 토큰은 | |
| 254 | - 테마별로 값이 바뀌지 않아 전환할 대상이 없다. */ | |
| 255 | -@media (prefers-reduced-motion: no-preference) { | |
| 256 | - html[data-theme-transition], | |
| 257 | - html[data-theme-transition] *, | |
| 258 | - html[data-theme-transition] *::before, | |
| 259 | - html[data-theme-transition] *::after { | |
| 260 | - transition: | |
| 261 | - background-color 200ms var(--ease-out), | |
| 262 | - border-color 200ms var(--ease-out), | |
| 263 | - color 200ms var(--ease-out), | |
| 264 | - fill 200ms var(--ease-out), | |
| 265 | - stroke 200ms var(--ease-out), | |
| 266 | - outline-color 200ms var(--ease-out) !important; | |
| 267 | - } | |
| 268 | -} | |
| 269 | - | |
| 270 | -body { | |
| 271 | - background: var(--background); | |
| 272 | - color: var(--foreground); | |
| 273 | -} |
+++ app/globals.scss
... | ... | @@ -0,0 +1,20 @@ |
| 1 | +// 앱 글로벌 스타일 — 디자인 시스템 로드 + **이 앱에만 해당하는** 최소 베이스. | |
| 2 | +// | |
| 3 | +// 색·타이포·간격 같은 디자인 값은 여기에 두지 않는다. 전부 `@fox/styles/tokens/`가 | |
| 4 | +// 소유하며, 이 파일은 "@fox를 켜고, 이 앱의 셸 구조를 잡는다"까지만 한다. | |
| 5 | +// 화면·컴포넌트 스타일은 각자의 `.module.scss`로 간다. | |
| 6 | + | |
| 7 | +@use "@fox/styles"; | |
| 8 | + | |
| 9 | +// 앱 셸 — 루트 레이아웃이 뷰포트 높이를 채우고 세로로 쌓이는 구조. | |
| 10 | +// 푸터를 바닥에 붙이거나 본문 영역만 스크롤시키는 화면들이 이 전제에 기댄다. | |
| 11 | +html, | |
| 12 | +body { | |
| 13 | + block-size: 100%; | |
| 14 | +} | |
| 15 | + | |
| 16 | +body { | |
| 17 | + display: flex; | |
| 18 | + flex-direction: column; | |
| 19 | + min-block-size: 100%; | |
| 20 | +} |
--- app/layout.tsx
+++ app/layout.tsx
... | ... | @@ -9,7 +9,7 @@ |
| 9 | 9 |
THEME_ATTRIBUTE_NAME, |
| 10 | 10 |
THEME_STORAGE_KEY, |
| 11 | 11 |
} from "@/lib/constants/theme"; |
| 12 |
-import "./globals.css"; |
|
| 12 |
+import "./globals.scss"; |
|
| 13 | 13 |
|
| 14 | 14 |
// 첫 페인트 전 테마 적용 — <head>는 <body>보다 먼저 파싱되므로 <html>만 조작 가능하다 |
| 15 | 15 |
// (preventing-flash-before-hydration.md의 Themes 패턴). localStorage에 명시 선택("light"
|
... | ... | @@ -41,9 +41,11 @@ |
| 41 | 41 |
SPLASH_SHOWN_STORAGE_KEY |
| 42 | 42 |
)}))document.documentElement.setAttribute("data-splash-shown","")}catch(e){}})()`;
|
| 43 | 43 |
|
| 44 |
-// 어드민 전체 기본 서체(edupay-app 일체화, design 전달 사양) — Geist는 대체되어 제거했다. |
|
| 45 |
-// globals.css `--font-sans`가 이 변수를 우선 참조하고, 로드 전에도 system-ui로 안전하게 |
|
| 46 |
-// 폴백하도록 이미 구성돼 있다. |
|
| 44 |
+// 어드민 전체 기본 서체. 폰트 파일은 **앱이 소유**하고 디자인 시스템은 참조만 한다 — |
|
| 45 |
+// `@fox`는 `--app-font-sans`를 읽을 뿐이라(@fox/README.md "호스트 앱과의 계약") 폴더를 |
|
| 46 |
+// 다른 프로젝트로 복사해도 그 프로젝트의 서체를 그대로 따른다. 변수명이 `--app-*`인 것도 |
|
| 47 |
+// 그 계약의 이름이기 때문이며, 미주입 시에도 `system-ui`로 폴백하도록 토큰 쪽에 폴백이 |
|
| 48 |
+// 들어 있다. |
|
| 47 | 49 |
const wantedSans = localFont({
|
| 48 | 50 |
src: [ |
| 49 | 51 |
{
|
... | ... | @@ -67,14 +69,13 @@ |
| 67 | 69 |
style: "normal", |
| 68 | 70 |
}, |
| 69 | 71 |
], |
| 70 |
- variable: "--font-wanted-sans", |
|
| 72 |
+ variable: "--app-font-sans", |
|
| 71 | 73 |
display: "swap", |
| 72 | 74 |
}); |
| 73 | 75 |
|
| 74 |
-// 코드/숫자 표기용 고정폭 서체 — globals.css `--font-mono`가 여전히 이 변수를 직접 참조하므로 |
|
| 75 |
-// (폴백 체인 없음) 유지한다. |
|
| 76 |
+// 코드/숫자 표기용 고정폭 서체 — sans와 동일하게 `--app-font-mono` 계약으로 주입한다. |
|
| 76 | 77 |
const geistMono = Geist_Mono({
|
| 77 |
- variable: "--font-geist-mono", |
|
| 78 |
+ variable: "--app-font-mono", |
|
| 78 | 79 |
subsets: ["latin"], |
| 79 | 80 |
}); |
| 80 | 81 |
|
... | ... | @@ -107,14 +108,14 @@ |
| 107 | 108 |
<html |
| 108 | 109 |
lang="ko" |
| 109 | 110 |
suppressHydrationWarning |
| 110 |
- className={`${wantedSans.variable} ${geistMono.variable} h-full antialiased`}
|
|
| 111 |
+ className={`${wantedSans.variable} ${geistMono.variable}`}
|
|
| 111 | 112 |
> |
| 112 | 113 |
<head> |
| 113 | 114 |
<script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
|
| 114 | 115 |
<style dangerouslySetInnerHTML={{ __html: SPLASH_HIDE_STYLE }} />
|
| 115 | 116 |
<script dangerouslySetInnerHTML={{ __html: SPLASH_GATE_SCRIPT }} />
|
| 116 | 117 |
</head> |
| 117 |
- <body className="min-h-full flex flex-col"> |
|
| 118 |
+ <body> |
|
| 118 | 119 |
<SplashGate /> |
| 119 | 120 |
<FeedbackProvider> |
| 120 | 121 |
{children}
|
--- next.config.ts
+++ next.config.ts
... | ... | @@ -1,3 +1,4 @@ |
| 1 |
+import path from "node:path"; |
|
| 1 | 2 |
import type { NextConfig } from "next";
|
| 2 | 3 |
|
| 3 | 4 |
// 다층 캐시·SEO 차단(§4)의 응답 헤더 계층. immutable 정적 자산(_next/static, _next/image)은 |
... | ... | @@ -19,6 +20,14 @@ |
| 19 | 20 |
]; |
| 20 | 21 |
|
| 21 | 22 |
const nextConfig: NextConfig = {
|
| 23 |
+ // SCSS 로드 경로 — 프로젝트 루트를 Sass의 loadPaths에 넣어 `@use "@fox/styles/abstracts"` |
|
| 24 |
+ // 같은 루트 기준 절대 경로가 해석되게 한다. 이게 없으면 컴포넌트마다 `../../../` 상대 |
|
| 25 |
+ // 경로를 써야 해서 @fox 폴더를 다른 프로젝트로 통째로 복사할 수 없다(포터블 요건). |
|
| 26 |
+ // tsconfig의 `@fox/*` alias는 TS/번들러 전용이라 Sass 해석에는 관여하지 않으므로, |
|
| 27 |
+ // 같은 경로를 여기서 한 번 더 선언해야 TS와 SCSS의 import 표기가 일치한다. |
|
| 28 |
+ sassOptions: {
|
|
| 29 |
+ loadPaths: [path.join(process.cwd())], |
|
| 30 |
+ }, |
|
| 22 | 31 |
async headers() {
|
| 23 | 32 |
return [ |
| 24 | 33 |
{
|
--- package-lock.json
+++ package-lock.json
... | ... | @@ -13,27 +13,13 @@ |
| 13 | 13 |
"react-dom": "19.2.4" |
| 14 | 14 |
}, |
| 15 | 15 |
"devDependencies": {
|
| 16 |
- "@tailwindcss/postcss": "^4", |
|
| 17 | 16 |
"@types/node": "^20", |
| 18 | 17 |
"@types/react": "^19", |
| 19 | 18 |
"@types/react-dom": "^19", |
| 20 | 19 |
"eslint": "^9", |
| 21 | 20 |
"eslint-config-next": "16.2.12", |
| 22 |
- "tailwindcss": "^4", |
|
| 21 |
+ "sass": "^1.102.0", |
|
| 23 | 22 |
"typescript": "^5" |
| 24 |
- } |
|
| 25 |
- }, |
|
| 26 |
- "node_modules/@alloc/quick-lru": {
|
|
| 27 |
- "version": "5.2.0", |
|
| 28 |
- "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", |
|
| 29 |
- "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", |
|
| 30 |
- "dev": true, |
|
| 31 |
- "license": "MIT", |
|
| 32 |
- "engines": {
|
|
| 33 |
- "node": ">=10" |
|
| 34 |
- }, |
|
| 35 |
- "funding": {
|
|
| 36 |
- "url": "https://github.com/sponsors/sindresorhus" |
|
| 37 | 23 |
} |
| 38 | 24 |
}, |
| 39 | 25 |
"node_modules/@babel/code-frame": {
|
... | ... | @@ -1246,6 +1232,308 @@ |
| 1246 | 1232 |
"node": ">=12.4.0" |
| 1247 | 1233 |
} |
| 1248 | 1234 |
}, |
| 1235 |
+ "node_modules/@parcel/watcher": {
|
|
| 1236 |
+ "version": "2.6.0", |
|
| 1237 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", |
|
| 1238 |
+ "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", |
|
| 1239 |
+ "dev": true, |
|
| 1240 |
+ "hasInstallScript": true, |
|
| 1241 |
+ "license": "MIT", |
|
| 1242 |
+ "optional": true, |
|
| 1243 |
+ "dependencies": {
|
|
| 1244 |
+ "detect-libc": "^2.0.3", |
|
| 1245 |
+ "is-glob": "^4.0.3", |
|
| 1246 |
+ "node-addon-api": "^7.0.0", |
|
| 1247 |
+ "picomatch": "^4.0.4" |
|
| 1248 |
+ }, |
|
| 1249 |
+ "engines": {
|
|
| 1250 |
+ "node": ">= 10.0.0" |
|
| 1251 |
+ }, |
|
| 1252 |
+ "funding": {
|
|
| 1253 |
+ "type": "opencollective", |
|
| 1254 |
+ "url": "https://opencollective.com/parcel" |
|
| 1255 |
+ }, |
|
| 1256 |
+ "optionalDependencies": {
|
|
| 1257 |
+ "@parcel/watcher-android-arm64": "2.6.0", |
|
| 1258 |
+ "@parcel/watcher-darwin-arm64": "2.6.0", |
|
| 1259 |
+ "@parcel/watcher-darwin-x64": "2.6.0", |
|
| 1260 |
+ "@parcel/watcher-freebsd-x64": "2.6.0", |
|
| 1261 |
+ "@parcel/watcher-linux-arm-glibc": "2.6.0", |
|
| 1262 |
+ "@parcel/watcher-linux-arm-musl": "2.6.0", |
|
| 1263 |
+ "@parcel/watcher-linux-arm64-glibc": "2.6.0", |
|
| 1264 |
+ "@parcel/watcher-linux-arm64-musl": "2.6.0", |
|
| 1265 |
+ "@parcel/watcher-linux-x64-glibc": "2.6.0", |
|
| 1266 |
+ "@parcel/watcher-linux-x64-musl": "2.6.0", |
|
| 1267 |
+ "@parcel/watcher-win32-arm64": "2.6.0", |
|
| 1268 |
+ "@parcel/watcher-win32-x64": "2.6.0" |
|
| 1269 |
+ } |
|
| 1270 |
+ }, |
|
| 1271 |
+ "node_modules/@parcel/watcher-android-arm64": {
|
|
| 1272 |
+ "version": "2.6.0", |
|
| 1273 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", |
|
| 1274 |
+ "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", |
|
| 1275 |
+ "cpu": [ |
|
| 1276 |
+ "arm64" |
|
| 1277 |
+ ], |
|
| 1278 |
+ "dev": true, |
|
| 1279 |
+ "license": "MIT", |
|
| 1280 |
+ "optional": true, |
|
| 1281 |
+ "os": [ |
|
| 1282 |
+ "android" |
|
| 1283 |
+ ], |
|
| 1284 |
+ "engines": {
|
|
| 1285 |
+ "node": ">= 10.0.0" |
|
| 1286 |
+ }, |
|
| 1287 |
+ "funding": {
|
|
| 1288 |
+ "type": "opencollective", |
|
| 1289 |
+ "url": "https://opencollective.com/parcel" |
|
| 1290 |
+ } |
|
| 1291 |
+ }, |
|
| 1292 |
+ "node_modules/@parcel/watcher-darwin-arm64": {
|
|
| 1293 |
+ "version": "2.6.0", |
|
| 1294 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", |
|
| 1295 |
+ "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", |
|
| 1296 |
+ "cpu": [ |
|
| 1297 |
+ "arm64" |
|
| 1298 |
+ ], |
|
| 1299 |
+ "dev": true, |
|
| 1300 |
+ "license": "MIT", |
|
| 1301 |
+ "optional": true, |
|
| 1302 |
+ "os": [ |
|
| 1303 |
+ "darwin" |
|
| 1304 |
+ ], |
|
| 1305 |
+ "engines": {
|
|
| 1306 |
+ "node": ">= 10.0.0" |
|
| 1307 |
+ }, |
|
| 1308 |
+ "funding": {
|
|
| 1309 |
+ "type": "opencollective", |
|
| 1310 |
+ "url": "https://opencollective.com/parcel" |
|
| 1311 |
+ } |
|
| 1312 |
+ }, |
|
| 1313 |
+ "node_modules/@parcel/watcher-darwin-x64": {
|
|
| 1314 |
+ "version": "2.6.0", |
|
| 1315 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", |
|
| 1316 |
+ "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", |
|
| 1317 |
+ "cpu": [ |
|
| 1318 |
+ "x64" |
|
| 1319 |
+ ], |
|
| 1320 |
+ "dev": true, |
|
| 1321 |
+ "license": "MIT", |
|
| 1322 |
+ "optional": true, |
|
| 1323 |
+ "os": [ |
|
| 1324 |
+ "darwin" |
|
| 1325 |
+ ], |
|
| 1326 |
+ "engines": {
|
|
| 1327 |
+ "node": ">= 10.0.0" |
|
| 1328 |
+ }, |
|
| 1329 |
+ "funding": {
|
|
| 1330 |
+ "type": "opencollective", |
|
| 1331 |
+ "url": "https://opencollective.com/parcel" |
|
| 1332 |
+ } |
|
| 1333 |
+ }, |
|
| 1334 |
+ "node_modules/@parcel/watcher-freebsd-x64": {
|
|
| 1335 |
+ "version": "2.6.0", |
|
| 1336 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", |
|
| 1337 |
+ "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", |
|
| 1338 |
+ "cpu": [ |
|
| 1339 |
+ "x64" |
|
| 1340 |
+ ], |
|
| 1341 |
+ "dev": true, |
|
| 1342 |
+ "license": "MIT", |
|
| 1343 |
+ "optional": true, |
|
| 1344 |
+ "os": [ |
|
| 1345 |
+ "freebsd" |
|
| 1346 |
+ ], |
|
| 1347 |
+ "engines": {
|
|
| 1348 |
+ "node": ">= 10.0.0" |
|
| 1349 |
+ }, |
|
| 1350 |
+ "funding": {
|
|
| 1351 |
+ "type": "opencollective", |
|
| 1352 |
+ "url": "https://opencollective.com/parcel" |
|
| 1353 |
+ } |
|
| 1354 |
+ }, |
|
| 1355 |
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
|
|
| 1356 |
+ "version": "2.6.0", |
|
| 1357 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", |
|
| 1358 |
+ "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", |
|
| 1359 |
+ "cpu": [ |
|
| 1360 |
+ "arm" |
|
| 1361 |
+ ], |
|
| 1362 |
+ "dev": true, |
|
| 1363 |
+ "license": "MIT", |
|
| 1364 |
+ "optional": true, |
|
| 1365 |
+ "os": [ |
|
| 1366 |
+ "linux" |
|
| 1367 |
+ ], |
|
| 1368 |
+ "engines": {
|
|
| 1369 |
+ "node": ">= 10.0.0" |
|
| 1370 |
+ }, |
|
| 1371 |
+ "funding": {
|
|
| 1372 |
+ "type": "opencollective", |
|
| 1373 |
+ "url": "https://opencollective.com/parcel" |
|
| 1374 |
+ } |
|
| 1375 |
+ }, |
|
| 1376 |
+ "node_modules/@parcel/watcher-linux-arm-musl": {
|
|
| 1377 |
+ "version": "2.6.0", |
|
| 1378 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", |
|
| 1379 |
+ "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", |
|
| 1380 |
+ "cpu": [ |
|
| 1381 |
+ "arm" |
|
| 1382 |
+ ], |
|
| 1383 |
+ "dev": true, |
|
| 1384 |
+ "license": "MIT", |
|
| 1385 |
+ "optional": true, |
|
| 1386 |
+ "os": [ |
|
| 1387 |
+ "linux" |
|
| 1388 |
+ ], |
|
| 1389 |
+ "engines": {
|
|
| 1390 |
+ "node": ">= 10.0.0" |
|
| 1391 |
+ }, |
|
| 1392 |
+ "funding": {
|
|
| 1393 |
+ "type": "opencollective", |
|
| 1394 |
+ "url": "https://opencollective.com/parcel" |
|
| 1395 |
+ } |
|
| 1396 |
+ }, |
|
| 1397 |
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
|
|
| 1398 |
+ "version": "2.6.0", |
|
| 1399 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", |
|
| 1400 |
+ "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", |
|
| 1401 |
+ "cpu": [ |
|
| 1402 |
+ "arm64" |
|
| 1403 |
+ ], |
|
| 1404 |
+ "dev": true, |
|
| 1405 |
+ "license": "MIT", |
|
| 1406 |
+ "optional": true, |
|
| 1407 |
+ "os": [ |
|
| 1408 |
+ "linux" |
|
| 1409 |
+ ], |
|
| 1410 |
+ "engines": {
|
|
| 1411 |
+ "node": ">= 10.0.0" |
|
| 1412 |
+ }, |
|
| 1413 |
+ "funding": {
|
|
| 1414 |
+ "type": "opencollective", |
|
| 1415 |
+ "url": "https://opencollective.com/parcel" |
|
| 1416 |
+ } |
|
| 1417 |
+ }, |
|
| 1418 |
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
|
|
| 1419 |
+ "version": "2.6.0", |
|
| 1420 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", |
|
| 1421 |
+ "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", |
|
| 1422 |
+ "cpu": [ |
|
| 1423 |
+ "arm64" |
|
| 1424 |
+ ], |
|
| 1425 |
+ "dev": true, |
|
| 1426 |
+ "license": "MIT", |
|
| 1427 |
+ "optional": true, |
|
| 1428 |
+ "os": [ |
|
| 1429 |
+ "linux" |
|
| 1430 |
+ ], |
|
| 1431 |
+ "engines": {
|
|
| 1432 |
+ "node": ">= 10.0.0" |
|
| 1433 |
+ }, |
|
| 1434 |
+ "funding": {
|
|
| 1435 |
+ "type": "opencollective", |
|
| 1436 |
+ "url": "https://opencollective.com/parcel" |
|
| 1437 |
+ } |
|
| 1438 |
+ }, |
|
| 1439 |
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
|
|
| 1440 |
+ "version": "2.6.0", |
|
| 1441 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", |
|
| 1442 |
+ "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", |
|
| 1443 |
+ "cpu": [ |
|
| 1444 |
+ "x64" |
|
| 1445 |
+ ], |
|
| 1446 |
+ "dev": true, |
|
| 1447 |
+ "license": "MIT", |
|
| 1448 |
+ "optional": true, |
|
| 1449 |
+ "os": [ |
|
| 1450 |
+ "linux" |
|
| 1451 |
+ ], |
|
| 1452 |
+ "engines": {
|
|
| 1453 |
+ "node": ">= 10.0.0" |
|
| 1454 |
+ }, |
|
| 1455 |
+ "funding": {
|
|
| 1456 |
+ "type": "opencollective", |
|
| 1457 |
+ "url": "https://opencollective.com/parcel" |
|
| 1458 |
+ } |
|
| 1459 |
+ }, |
|
| 1460 |
+ "node_modules/@parcel/watcher-linux-x64-musl": {
|
|
| 1461 |
+ "version": "2.6.0", |
|
| 1462 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", |
|
| 1463 |
+ "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", |
|
| 1464 |
+ "cpu": [ |
|
| 1465 |
+ "x64" |
|
| 1466 |
+ ], |
|
| 1467 |
+ "dev": true, |
|
| 1468 |
+ "license": "MIT", |
|
| 1469 |
+ "optional": true, |
|
| 1470 |
+ "os": [ |
|
| 1471 |
+ "linux" |
|
| 1472 |
+ ], |
|
| 1473 |
+ "engines": {
|
|
| 1474 |
+ "node": ">= 10.0.0" |
|
| 1475 |
+ }, |
|
| 1476 |
+ "funding": {
|
|
| 1477 |
+ "type": "opencollective", |
|
| 1478 |
+ "url": "https://opencollective.com/parcel" |
|
| 1479 |
+ } |
|
| 1480 |
+ }, |
|
| 1481 |
+ "node_modules/@parcel/watcher-win32-arm64": {
|
|
| 1482 |
+ "version": "2.6.0", |
|
| 1483 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", |
|
| 1484 |
+ "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", |
|
| 1485 |
+ "cpu": [ |
|
| 1486 |
+ "arm64" |
|
| 1487 |
+ ], |
|
| 1488 |
+ "dev": true, |
|
| 1489 |
+ "license": "MIT", |
|
| 1490 |
+ "optional": true, |
|
| 1491 |
+ "os": [ |
|
| 1492 |
+ "win32" |
|
| 1493 |
+ ], |
|
| 1494 |
+ "engines": {
|
|
| 1495 |
+ "node": ">= 10.0.0" |
|
| 1496 |
+ }, |
|
| 1497 |
+ "funding": {
|
|
| 1498 |
+ "type": "opencollective", |
|
| 1499 |
+ "url": "https://opencollective.com/parcel" |
|
| 1500 |
+ } |
|
| 1501 |
+ }, |
|
| 1502 |
+ "node_modules/@parcel/watcher-win32-x64": {
|
|
| 1503 |
+ "version": "2.6.0", |
|
| 1504 |
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", |
|
| 1505 |
+ "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", |
|
| 1506 |
+ "cpu": [ |
|
| 1507 |
+ "x64" |
|
| 1508 |
+ ], |
|
| 1509 |
+ "dev": true, |
|
| 1510 |
+ "license": "MIT", |
|
| 1511 |
+ "optional": true, |
|
| 1512 |
+ "os": [ |
|
| 1513 |
+ "win32" |
|
| 1514 |
+ ], |
|
| 1515 |
+ "engines": {
|
|
| 1516 |
+ "node": ">= 10.0.0" |
|
| 1517 |
+ }, |
|
| 1518 |
+ "funding": {
|
|
| 1519 |
+ "type": "opencollective", |
|
| 1520 |
+ "url": "https://opencollective.com/parcel" |
|
| 1521 |
+ } |
|
| 1522 |
+ }, |
|
| 1523 |
+ "node_modules/@parcel/watcher/node_modules/picomatch": {
|
|
| 1524 |
+ "version": "4.0.5", |
|
| 1525 |
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", |
|
| 1526 |
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", |
|
| 1527 |
+ "dev": true, |
|
| 1528 |
+ "license": "MIT", |
|
| 1529 |
+ "optional": true, |
|
| 1530 |
+ "engines": {
|
|
| 1531 |
+ "node": ">=12" |
|
| 1532 |
+ }, |
|
| 1533 |
+ "funding": {
|
|
| 1534 |
+ "url": "https://github.com/sponsors/jonschlinkert" |
|
| 1535 |
+ } |
|
| 1536 |
+ }, |
|
| 1249 | 1537 |
"node_modules/@rtsao/scc": {
|
| 1250 | 1538 |
"version": "1.1.0", |
| 1251 | 1539 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", |
... | ... | @@ -1260,277 +1548,6 @@ |
| 1260 | 1548 |
"license": "Apache-2.0", |
| 1261 | 1549 |
"dependencies": {
|
| 1262 | 1550 |
"tslib": "^2.8.0" |
| 1263 |
- } |
|
| 1264 |
- }, |
|
| 1265 |
- "node_modules/@tailwindcss/node": {
|
|
| 1266 |
- "version": "4.3.3", |
|
| 1267 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", |
|
| 1268 |
- "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", |
|
| 1269 |
- "dev": true, |
|
| 1270 |
- "license": "MIT", |
|
| 1271 |
- "dependencies": {
|
|
| 1272 |
- "@jridgewell/remapping": "^2.3.5", |
|
| 1273 |
- "enhanced-resolve": "^5.24.1", |
|
| 1274 |
- "jiti": "^2.7.0", |
|
| 1275 |
- "lightningcss": "1.32.0", |
|
| 1276 |
- "magic-string": "^0.30.21", |
|
| 1277 |
- "source-map-js": "^1.2.1", |
|
| 1278 |
- "tailwindcss": "4.3.3" |
|
| 1279 |
- } |
|
| 1280 |
- }, |
|
| 1281 |
- "node_modules/@tailwindcss/oxide": {
|
|
| 1282 |
- "version": "4.3.3", |
|
| 1283 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", |
|
| 1284 |
- "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", |
|
| 1285 |
- "dev": true, |
|
| 1286 |
- "license": "MIT", |
|
| 1287 |
- "engines": {
|
|
| 1288 |
- "node": ">= 20" |
|
| 1289 |
- }, |
|
| 1290 |
- "optionalDependencies": {
|
|
| 1291 |
- "@tailwindcss/oxide-android-arm64": "4.3.3", |
|
| 1292 |
- "@tailwindcss/oxide-darwin-arm64": "4.3.3", |
|
| 1293 |
- "@tailwindcss/oxide-darwin-x64": "4.3.3", |
|
| 1294 |
- "@tailwindcss/oxide-freebsd-x64": "4.3.3", |
|
| 1295 |
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", |
|
| 1296 |
- "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", |
|
| 1297 |
- "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", |
|
| 1298 |
- "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", |
|
| 1299 |
- "@tailwindcss/oxide-linux-x64-musl": "4.3.3", |
|
| 1300 |
- "@tailwindcss/oxide-wasm32-wasi": "4.3.3", |
|
| 1301 |
- "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", |
|
| 1302 |
- "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" |
|
| 1303 |
- } |
|
| 1304 |
- }, |
|
| 1305 |
- "node_modules/@tailwindcss/oxide-android-arm64": {
|
|
| 1306 |
- "version": "4.3.3", |
|
| 1307 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", |
|
| 1308 |
- "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", |
|
| 1309 |
- "cpu": [ |
|
| 1310 |
- "arm64" |
|
| 1311 |
- ], |
|
| 1312 |
- "dev": true, |
|
| 1313 |
- "license": "MIT", |
|
| 1314 |
- "optional": true, |
|
| 1315 |
- "os": [ |
|
| 1316 |
- "android" |
|
| 1317 |
- ], |
|
| 1318 |
- "engines": {
|
|
| 1319 |
- "node": ">= 20" |
|
| 1320 |
- } |
|
| 1321 |
- }, |
|
| 1322 |
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
|
|
| 1323 |
- "version": "4.3.3", |
|
| 1324 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", |
|
| 1325 |
- "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", |
|
| 1326 |
- "cpu": [ |
|
| 1327 |
- "arm64" |
|
| 1328 |
- ], |
|
| 1329 |
- "dev": true, |
|
| 1330 |
- "license": "MIT", |
|
| 1331 |
- "optional": true, |
|
| 1332 |
- "os": [ |
|
| 1333 |
- "darwin" |
|
| 1334 |
- ], |
|
| 1335 |
- "engines": {
|
|
| 1336 |
- "node": ">= 20" |
|
| 1337 |
- } |
|
| 1338 |
- }, |
|
| 1339 |
- "node_modules/@tailwindcss/oxide-darwin-x64": {
|
|
| 1340 |
- "version": "4.3.3", |
|
| 1341 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", |
|
| 1342 |
- "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", |
|
| 1343 |
- "cpu": [ |
|
| 1344 |
- "x64" |
|
| 1345 |
- ], |
|
| 1346 |
- "dev": true, |
|
| 1347 |
- "license": "MIT", |
|
| 1348 |
- "optional": true, |
|
| 1349 |
- "os": [ |
|
| 1350 |
- "darwin" |
|
| 1351 |
- ], |
|
| 1352 |
- "engines": {
|
|
| 1353 |
- "node": ">= 20" |
|
| 1354 |
- } |
|
| 1355 |
- }, |
|
| 1356 |
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
|
|
| 1357 |
- "version": "4.3.3", |
|
| 1358 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", |
|
| 1359 |
- "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", |
|
| 1360 |
- "cpu": [ |
|
| 1361 |
- "x64" |
|
| 1362 |
- ], |
|
| 1363 |
- "dev": true, |
|
| 1364 |
- "license": "MIT", |
|
| 1365 |
- "optional": true, |
|
| 1366 |
- "os": [ |
|
| 1367 |
- "freebsd" |
|
| 1368 |
- ], |
|
| 1369 |
- "engines": {
|
|
| 1370 |
- "node": ">= 20" |
|
| 1371 |
- } |
|
| 1372 |
- }, |
|
| 1373 |
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
|
| 1374 |
- "version": "4.3.3", |
|
| 1375 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", |
|
| 1376 |
- "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", |
|
| 1377 |
- "cpu": [ |
|
| 1378 |
- "arm" |
|
| 1379 |
- ], |
|
| 1380 |
- "dev": true, |
|
| 1381 |
- "license": "MIT", |
|
| 1382 |
- "optional": true, |
|
| 1383 |
- "os": [ |
|
| 1384 |
- "linux" |
|
| 1385 |
- ], |
|
| 1386 |
- "engines": {
|
|
| 1387 |
- "node": ">= 20" |
|
| 1388 |
- } |
|
| 1389 |
- }, |
|
| 1390 |
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
|
| 1391 |
- "version": "4.3.3", |
|
| 1392 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", |
|
| 1393 |
- "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", |
|
| 1394 |
- "cpu": [ |
|
| 1395 |
- "arm64" |
|
| 1396 |
- ], |
|
| 1397 |
- "dev": true, |
|
| 1398 |
- "license": "MIT", |
|
| 1399 |
- "optional": true, |
|
| 1400 |
- "os": [ |
|
| 1401 |
- "linux" |
|
| 1402 |
- ], |
|
| 1403 |
- "engines": {
|
|
| 1404 |
- "node": ">= 20" |
|
| 1405 |
- } |
|
| 1406 |
- }, |
|
| 1407 |
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
|
| 1408 |
- "version": "4.3.3", |
|
| 1409 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", |
|
| 1410 |
- "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", |
|
| 1411 |
- "cpu": [ |
|
| 1412 |
- "arm64" |
|
| 1413 |
- ], |
|
| 1414 |
- "dev": true, |
|
| 1415 |
- "license": "MIT", |
|
| 1416 |
- "optional": true, |
|
| 1417 |
- "os": [ |
|
| 1418 |
- "linux" |
|
| 1419 |
- ], |
|
| 1420 |
- "engines": {
|
|
| 1421 |
- "node": ">= 20" |
|
| 1422 |
- } |
|
| 1423 |
- }, |
|
| 1424 |
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
|
| 1425 |
- "version": "4.3.3", |
|
| 1426 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", |
|
| 1427 |
- "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", |
|
| 1428 |
- "cpu": [ |
|
| 1429 |
- "x64" |
|
| 1430 |
- ], |
|
| 1431 |
- "dev": true, |
|
| 1432 |
- "license": "MIT", |
|
| 1433 |
- "optional": true, |
|
| 1434 |
- "os": [ |
|
| 1435 |
- "linux" |
|
| 1436 |
- ], |
|
| 1437 |
- "engines": {
|
|
| 1438 |
- "node": ">= 20" |
|
| 1439 |
- } |
|
| 1440 |
- }, |
|
| 1441 |
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
|
| 1442 |
- "version": "4.3.3", |
|
| 1443 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", |
|
| 1444 |
- "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", |
|
| 1445 |
- "cpu": [ |
|
| 1446 |
- "x64" |
|
| 1447 |
- ], |
|
| 1448 |
- "dev": true, |
|
| 1449 |
- "license": "MIT", |
|
| 1450 |
- "optional": true, |
|
| 1451 |
- "os": [ |
|
| 1452 |
- "linux" |
|
| 1453 |
- ], |
|
| 1454 |
- "engines": {
|
|
| 1455 |
- "node": ">= 20" |
|
| 1456 |
- } |
|
| 1457 |
- }, |
|
| 1458 |
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
|
| 1459 |
- "version": "4.3.3", |
|
| 1460 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", |
|
| 1461 |
- "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", |
|
| 1462 |
- "bundleDependencies": [ |
|
| 1463 |
- "@napi-rs/wasm-runtime", |
|
| 1464 |
- "@emnapi/core", |
|
| 1465 |
- "@emnapi/runtime", |
|
| 1466 |
- "@tybys/wasm-util", |
|
| 1467 |
- "@emnapi/wasi-threads", |
|
| 1468 |
- "tslib" |
|
| 1469 |
- ], |
|
| 1470 |
- "cpu": [ |
|
| 1471 |
- "wasm32" |
|
| 1472 |
- ], |
|
| 1473 |
- "dev": true, |
|
| 1474 |
- "license": "MIT", |
|
| 1475 |
- "optional": true, |
|
| 1476 |
- "dependencies": {
|
|
| 1477 |
- "@emnapi/core": "^1.11.1", |
|
| 1478 |
- "@emnapi/runtime": "^1.11.1", |
|
| 1479 |
- "@emnapi/wasi-threads": "^1.2.2", |
|
| 1480 |
- "@napi-rs/wasm-runtime": "^1.1.4", |
|
| 1481 |
- "@tybys/wasm-util": "^0.10.2", |
|
| 1482 |
- "tslib": "^2.8.1" |
|
| 1483 |
- }, |
|
| 1484 |
- "engines": {
|
|
| 1485 |
- "node": ">=14.0.0" |
|
| 1486 |
- } |
|
| 1487 |
- }, |
|
| 1488 |
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
|
| 1489 |
- "version": "4.3.3", |
|
| 1490 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", |
|
| 1491 |
- "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", |
|
| 1492 |
- "cpu": [ |
|
| 1493 |
- "arm64" |
|
| 1494 |
- ], |
|
| 1495 |
- "dev": true, |
|
| 1496 |
- "license": "MIT", |
|
| 1497 |
- "optional": true, |
|
| 1498 |
- "os": [ |
|
| 1499 |
- "win32" |
|
| 1500 |
- ], |
|
| 1501 |
- "engines": {
|
|
| 1502 |
- "node": ">= 20" |
|
| 1503 |
- } |
|
| 1504 |
- }, |
|
| 1505 |
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
|
| 1506 |
- "version": "4.3.3", |
|
| 1507 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", |
|
| 1508 |
- "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", |
|
| 1509 |
- "cpu": [ |
|
| 1510 |
- "x64" |
|
| 1511 |
- ], |
|
| 1512 |
- "dev": true, |
|
| 1513 |
- "license": "MIT", |
|
| 1514 |
- "optional": true, |
|
| 1515 |
- "os": [ |
|
| 1516 |
- "win32" |
|
| 1517 |
- ], |
|
| 1518 |
- "engines": {
|
|
| 1519 |
- "node": ">= 20" |
|
| 1520 |
- } |
|
| 1521 |
- }, |
|
| 1522 |
- "node_modules/@tailwindcss/postcss": {
|
|
| 1523 |
- "version": "4.3.3", |
|
| 1524 |
- "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", |
|
| 1525 |
- "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", |
|
| 1526 |
- "dev": true, |
|
| 1527 |
- "license": "MIT", |
|
| 1528 |
- "dependencies": {
|
|
| 1529 |
- "@alloc/quick-lru": "^5.2.0", |
|
| 1530 |
- "@tailwindcss/node": "4.3.3", |
|
| 1531 |
- "@tailwindcss/oxide": "4.3.3", |
|
| 1532 |
- "postcss": "^8.5.16", |
|
| 1533 |
- "tailwindcss": "4.3.3" |
|
| 1534 | 1551 |
} |
| 1535 | 1552 |
}, |
| 1536 | 1553 |
"node_modules/@tybys/wasm-util": {
|
... | ... | @@ -2674,6 +2691,22 @@ |
| 2674 | 2691 |
"url": "https://github.com/chalk/chalk?sponsor=1" |
| 2675 | 2692 |
} |
| 2676 | 2693 |
}, |
| 2694 |
+ "node_modules/chokidar": {
|
|
| 2695 |
+ "version": "5.0.0", |
|
| 2696 |
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", |
|
| 2697 |
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", |
|
| 2698 |
+ "devOptional": true, |
|
| 2699 |
+ "license": "MIT", |
|
| 2700 |
+ "dependencies": {
|
|
| 2701 |
+ "readdirp": "^5.0.0" |
|
| 2702 |
+ }, |
|
| 2703 |
+ "engines": {
|
|
| 2704 |
+ "node": ">= 20.19.0" |
|
| 2705 |
+ }, |
|
| 2706 |
+ "funding": {
|
|
| 2707 |
+ "url": "https://paulmillr.com/funding/" |
|
| 2708 |
+ } |
|
| 2709 |
+ }, |
|
| 2677 | 2710 |
"node_modules/client-only": {
|
| 2678 | 2711 |
"version": "0.0.1", |
| 2679 | 2712 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", |
... | ... | @@ -2862,8 +2895,8 @@ |
| 2862 | 2895 |
"version": "2.1.2", |
| 2863 | 2896 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", |
| 2864 | 2897 |
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", |
| 2865 |
- "devOptional": true, |
|
| 2866 | 2898 |
"license": "Apache-2.0", |
| 2899 |
+ "optional": true, |
|
| 2867 | 2900 |
"engines": {
|
| 2868 | 2901 |
"node": ">=8" |
| 2869 | 2902 |
} |
... | ... | @@ -2909,20 +2942,6 @@ |
| 2909 | 2942 |
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", |
| 2910 | 2943 |
"dev": true, |
| 2911 | 2944 |
"license": "MIT" |
| 2912 |
- }, |
|
| 2913 |
- "node_modules/enhanced-resolve": {
|
|
| 2914 |
- "version": "5.24.3", |
|
| 2915 |
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", |
|
| 2916 |
- "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", |
|
| 2917 |
- "dev": true, |
|
| 2918 |
- "license": "MIT", |
|
| 2919 |
- "dependencies": {
|
|
| 2920 |
- "graceful-fs": "^4.2.4", |
|
| 2921 |
- "tapable": "^2.3.3" |
|
| 2922 |
- }, |
|
| 2923 |
- "engines": {
|
|
| 2924 |
- "node": ">=10.13.0" |
|
| 2925 |
- } |
|
| 2926 | 2945 |
}, |
| 2927 | 2946 |
"node_modules/es-abstract": {
|
| 2928 | 2947 |
"version": "1.24.2", |
... | ... | @@ -3883,13 +3902,6 @@ |
| 3883 | 3902 |
"url": "https://github.com/sponsors/ljharb" |
| 3884 | 3903 |
} |
| 3885 | 3904 |
}, |
| 3886 |
- "node_modules/graceful-fs": {
|
|
| 3887 |
- "version": "4.2.11", |
|
| 3888 |
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", |
|
| 3889 |
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", |
|
| 3890 |
- "dev": true, |
|
| 3891 |
- "license": "ISC" |
|
| 3892 |
- }, |
|
| 3893 | 3905 |
"node_modules/has-bigints": {
|
| 3894 | 3906 |
"version": "1.1.0", |
| 3895 | 3907 |
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", |
... | ... | @@ -4010,6 +4022,13 @@ |
| 4010 | 4022 |
"engines": {
|
| 4011 | 4023 |
"node": ">= 4" |
| 4012 | 4024 |
} |
| 4025 |
+ }, |
|
| 4026 |
+ "node_modules/immutable": {
|
|
| 4027 |
+ "version": "5.1.9", |
|
| 4028 |
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", |
|
| 4029 |
+ "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", |
|
| 4030 |
+ "devOptional": true, |
|
| 4031 |
+ "license": "MIT" |
|
| 4013 | 4032 |
}, |
| 4014 | 4033 |
"node_modules/import-fresh": {
|
| 4015 | 4034 |
"version": "3.3.1", |
... | ... | @@ -4522,6 +4541,8 @@ |
| 4522 | 4541 |
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", |
| 4523 | 4542 |
"dev": true, |
| 4524 | 4543 |
"license": "MIT", |
| 4544 |
+ "optional": true, |
|
| 4545 |
+ "peer": true, |
|
| 4525 | 4546 |
"bin": {
|
| 4526 | 4547 |
"jiti": "lib/jiti-cli.mjs" |
| 4527 | 4548 |
} |
... | ... | @@ -4663,267 +4684,6 @@ |
| 4663 | 4684 |
"node": ">= 0.8.0" |
| 4664 | 4685 |
} |
| 4665 | 4686 |
}, |
| 4666 |
- "node_modules/lightningcss": {
|
|
| 4667 |
- "version": "1.32.0", |
|
| 4668 |
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", |
|
| 4669 |
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", |
|
| 4670 |
- "dev": true, |
|
| 4671 |
- "license": "MPL-2.0", |
|
| 4672 |
- "dependencies": {
|
|
| 4673 |
- "detect-libc": "^2.0.3" |
|
| 4674 |
- }, |
|
| 4675 |
- "engines": {
|
|
| 4676 |
- "node": ">= 12.0.0" |
|
| 4677 |
- }, |
|
| 4678 |
- "funding": {
|
|
| 4679 |
- "type": "opencollective", |
|
| 4680 |
- "url": "https://opencollective.com/parcel" |
|
| 4681 |
- }, |
|
| 4682 |
- "optionalDependencies": {
|
|
| 4683 |
- "lightningcss-android-arm64": "1.32.0", |
|
| 4684 |
- "lightningcss-darwin-arm64": "1.32.0", |
|
| 4685 |
- "lightningcss-darwin-x64": "1.32.0", |
|
| 4686 |
- "lightningcss-freebsd-x64": "1.32.0", |
|
| 4687 |
- "lightningcss-linux-arm-gnueabihf": "1.32.0", |
|
| 4688 |
- "lightningcss-linux-arm64-gnu": "1.32.0", |
|
| 4689 |
- "lightningcss-linux-arm64-musl": "1.32.0", |
|
| 4690 |
- "lightningcss-linux-x64-gnu": "1.32.0", |
|
| 4691 |
- "lightningcss-linux-x64-musl": "1.32.0", |
|
| 4692 |
- "lightningcss-win32-arm64-msvc": "1.32.0", |
|
| 4693 |
- "lightningcss-win32-x64-msvc": "1.32.0" |
|
| 4694 |
- } |
|
| 4695 |
- }, |
|
| 4696 |
- "node_modules/lightningcss-android-arm64": {
|
|
| 4697 |
- "version": "1.32.0", |
|
| 4698 |
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", |
|
| 4699 |
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", |
|
| 4700 |
- "cpu": [ |
|
| 4701 |
- "arm64" |
|
| 4702 |
- ], |
|
| 4703 |
- "dev": true, |
|
| 4704 |
- "license": "MPL-2.0", |
|
| 4705 |
- "optional": true, |
|
| 4706 |
- "os": [ |
|
| 4707 |
- "android" |
|
| 4708 |
- ], |
|
| 4709 |
- "engines": {
|
|
| 4710 |
- "node": ">= 12.0.0" |
|
| 4711 |
- }, |
|
| 4712 |
- "funding": {
|
|
| 4713 |
- "type": "opencollective", |
|
| 4714 |
- "url": "https://opencollective.com/parcel" |
|
| 4715 |
- } |
|
| 4716 |
- }, |
|
| 4717 |
- "node_modules/lightningcss-darwin-arm64": {
|
|
| 4718 |
- "version": "1.32.0", |
|
| 4719 |
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", |
|
| 4720 |
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", |
|
| 4721 |
- "cpu": [ |
|
| 4722 |
- "arm64" |
|
| 4723 |
- ], |
|
| 4724 |
- "dev": true, |
|
| 4725 |
- "license": "MPL-2.0", |
|
| 4726 |
- "optional": true, |
|
| 4727 |
- "os": [ |
|
| 4728 |
- "darwin" |
|
| 4729 |
- ], |
|
| 4730 |
- "engines": {
|
|
| 4731 |
- "node": ">= 12.0.0" |
|
| 4732 |
- }, |
|
| 4733 |
- "funding": {
|
|
| 4734 |
- "type": "opencollective", |
|
| 4735 |
- "url": "https://opencollective.com/parcel" |
|
| 4736 |
- } |
|
| 4737 |
- }, |
|
| 4738 |
- "node_modules/lightningcss-darwin-x64": {
|
|
| 4739 |
- "version": "1.32.0", |
|
| 4740 |
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", |
|
| 4741 |
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", |
|
| 4742 |
- "cpu": [ |
|
| 4743 |
- "x64" |
|
| 4744 |
- ], |
|
| 4745 |
- "dev": true, |
|
| 4746 |
- "license": "MPL-2.0", |
|
| 4747 |
- "optional": true, |
|
| 4748 |
- "os": [ |
|
| 4749 |
- "darwin" |
|
| 4750 |
- ], |
|
| 4751 |
- "engines": {
|
|
| 4752 |
- "node": ">= 12.0.0" |
|
| 4753 |
- }, |
|
| 4754 |
- "funding": {
|
|
| 4755 |
- "type": "opencollective", |
|
| 4756 |
- "url": "https://opencollective.com/parcel" |
|
| 4757 |
- } |
|
| 4758 |
- }, |
|
| 4759 |
- "node_modules/lightningcss-freebsd-x64": {
|
|
| 4760 |
- "version": "1.32.0", |
|
| 4761 |
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", |
|
| 4762 |
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", |
|
| 4763 |
- "cpu": [ |
|
| 4764 |
- "x64" |
|
| 4765 |
- ], |
|
| 4766 |
- "dev": true, |
|
| 4767 |
- "license": "MPL-2.0", |
|
| 4768 |
- "optional": true, |
|
| 4769 |
- "os": [ |
|
| 4770 |
- "freebsd" |
|
| 4771 |
- ], |
|
| 4772 |
- "engines": {
|
|
| 4773 |
- "node": ">= 12.0.0" |
|
| 4774 |
- }, |
|
| 4775 |
- "funding": {
|
|
| 4776 |
- "type": "opencollective", |
|
| 4777 |
- "url": "https://opencollective.com/parcel" |
|
| 4778 |
- } |
|
| 4779 |
- }, |
|
| 4780 |
- "node_modules/lightningcss-linux-arm-gnueabihf": {
|
|
| 4781 |
- "version": "1.32.0", |
|
| 4782 |
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", |
|
| 4783 |
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", |
|
| 4784 |
- "cpu": [ |
|
| 4785 |
- "arm" |
|
| 4786 |
- ], |
|
| 4787 |
- "dev": true, |
|
| 4788 |
- "license": "MPL-2.0", |
|
| 4789 |
- "optional": true, |
|
| 4790 |
- "os": [ |
|
| 4791 |
- "linux" |
|
| 4792 |
- ], |
|
| 4793 |
- "engines": {
|
|
| 4794 |
- "node": ">= 12.0.0" |
|
| 4795 |
- }, |
|
| 4796 |
- "funding": {
|
|
| 4797 |
- "type": "opencollective", |
|
| 4798 |
- "url": "https://opencollective.com/parcel" |
|
| 4799 |
- } |
|
| 4800 |
- }, |
|
| 4801 |
- "node_modules/lightningcss-linux-arm64-gnu": {
|
|
| 4802 |
- "version": "1.32.0", |
|
| 4803 |
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", |
|
| 4804 |
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", |
|
| 4805 |
- "cpu": [ |
|
| 4806 |
- "arm64" |
|
| 4807 |
- ], |
|
| 4808 |
- "dev": true, |
|
| 4809 |
- "license": "MPL-2.0", |
|
| 4810 |
- "optional": true, |
|
| 4811 |
- "os": [ |
|
| 4812 |
- "linux" |
|
| 4813 |
- ], |
|
| 4814 |
- "engines": {
|
|
| 4815 |
- "node": ">= 12.0.0" |
|
| 4816 |
- }, |
|
| 4817 |
- "funding": {
|
|
| 4818 |
- "type": "opencollective", |
|
| 4819 |
- "url": "https://opencollective.com/parcel" |
|
| 4820 |
- } |
|
| 4821 |
- }, |
|
| 4822 |
- "node_modules/lightningcss-linux-arm64-musl": {
|
|
| 4823 |
- "version": "1.32.0", |
|
| 4824 |
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", |
|
| 4825 |
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", |
|
| 4826 |
- "cpu": [ |
|
| 4827 |
- "arm64" |
|
| 4828 |
- ], |
|
| 4829 |
- "dev": true, |
|
| 4830 |
- "license": "MPL-2.0", |
|
| 4831 |
- "optional": true, |
|
| 4832 |
- "os": [ |
|
| 4833 |
- "linux" |
|
| 4834 |
- ], |
|
| 4835 |
- "engines": {
|
|
| 4836 |
- "node": ">= 12.0.0" |
|
| 4837 |
- }, |
|
| 4838 |
- "funding": {
|
|
| 4839 |
- "type": "opencollective", |
|
| 4840 |
- "url": "https://opencollective.com/parcel" |
|
| 4841 |
- } |
|
| 4842 |
- }, |
|
| 4843 |
- "node_modules/lightningcss-linux-x64-gnu": {
|
|
| 4844 |
- "version": "1.32.0", |
|
| 4845 |
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", |
|
| 4846 |
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", |
|
| 4847 |
- "cpu": [ |
|
| 4848 |
- "x64" |
|
| 4849 |
- ], |
|
| 4850 |
- "dev": true, |
|
| 4851 |
- "license": "MPL-2.0", |
|
| 4852 |
- "optional": true, |
|
| 4853 |
- "os": [ |
|
| 4854 |
- "linux" |
|
| 4855 |
- ], |
|
| 4856 |
- "engines": {
|
|
| 4857 |
- "node": ">= 12.0.0" |
|
| 4858 |
- }, |
|
| 4859 |
- "funding": {
|
|
| 4860 |
- "type": "opencollective", |
|
| 4861 |
- "url": "https://opencollective.com/parcel" |
|
| 4862 |
- } |
|
| 4863 |
- }, |
|
| 4864 |
- "node_modules/lightningcss-linux-x64-musl": {
|
|
| 4865 |
- "version": "1.32.0", |
|
| 4866 |
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", |
|
| 4867 |
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", |
|
| 4868 |
- "cpu": [ |
|
| 4869 |
- "x64" |
|
| 4870 |
- ], |
|
| 4871 |
- "dev": true, |
|
| 4872 |
- "license": "MPL-2.0", |
|
| 4873 |
- "optional": true, |
|
| 4874 |
- "os": [ |
|
| 4875 |
- "linux" |
|
| 4876 |
- ], |
|
| 4877 |
- "engines": {
|
|
| 4878 |
- "node": ">= 12.0.0" |
|
| 4879 |
- }, |
|
| 4880 |
- "funding": {
|
|
| 4881 |
- "type": "opencollective", |
|
| 4882 |
- "url": "https://opencollective.com/parcel" |
|
| 4883 |
- } |
|
| 4884 |
- }, |
|
| 4885 |
- "node_modules/lightningcss-win32-arm64-msvc": {
|
|
| 4886 |
- "version": "1.32.0", |
|
| 4887 |
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", |
|
| 4888 |
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", |
|
| 4889 |
- "cpu": [ |
|
| 4890 |
- "arm64" |
|
| 4891 |
- ], |
|
| 4892 |
- "dev": true, |
|
| 4893 |
- "license": "MPL-2.0", |
|
| 4894 |
- "optional": true, |
|
| 4895 |
- "os": [ |
|
| 4896 |
- "win32" |
|
| 4897 |
- ], |
|
| 4898 |
- "engines": {
|
|
| 4899 |
- "node": ">= 12.0.0" |
|
| 4900 |
- }, |
|
| 4901 |
- "funding": {
|
|
| 4902 |
- "type": "opencollective", |
|
| 4903 |
- "url": "https://opencollective.com/parcel" |
|
| 4904 |
- } |
|
| 4905 |
- }, |
|
| 4906 |
- "node_modules/lightningcss-win32-x64-msvc": {
|
|
| 4907 |
- "version": "1.32.0", |
|
| 4908 |
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", |
|
| 4909 |
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", |
|
| 4910 |
- "cpu": [ |
|
| 4911 |
- "x64" |
|
| 4912 |
- ], |
|
| 4913 |
- "dev": true, |
|
| 4914 |
- "license": "MPL-2.0", |
|
| 4915 |
- "optional": true, |
|
| 4916 |
- "os": [ |
|
| 4917 |
- "win32" |
|
| 4918 |
- ], |
|
| 4919 |
- "engines": {
|
|
| 4920 |
- "node": ">= 12.0.0" |
|
| 4921 |
- }, |
|
| 4922 |
- "funding": {
|
|
| 4923 |
- "type": "opencollective", |
|
| 4924 |
- "url": "https://opencollective.com/parcel" |
|
| 4925 |
- } |
|
| 4926 |
- }, |
|
| 4927 | 4687 |
"node_modules/locate-path": {
|
| 4928 | 4688 |
"version": "6.0.0", |
| 4929 | 4689 |
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", |
... | ... | @@ -4968,16 +4728,6 @@ |
| 4968 | 4728 |
"license": "ISC", |
| 4969 | 4729 |
"dependencies": {
|
| 4970 | 4730 |
"yallist": "^3.0.2" |
| 4971 |
- } |
|
| 4972 |
- }, |
|
| 4973 |
- "node_modules/magic-string": {
|
|
| 4974 |
- "version": "0.30.21", |
|
| 4975 |
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", |
|
| 4976 |
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", |
|
| 4977 |
- "dev": true, |
|
| 4978 |
- "license": "MIT", |
|
| 4979 |
- "dependencies": {
|
|
| 4980 |
- "@jridgewell/sourcemap-codec": "^1.5.5" |
|
| 4981 | 4731 |
} |
| 4982 | 4732 |
}, |
| 4983 | 4733 |
"node_modules/math-intrinsics": {
|
... | ... | @@ -5165,6 +4915,14 @@ |
| 5165 | 4915 |
"engines": {
|
| 5166 | 4916 |
"node": "^10 || ^12 || >=14" |
| 5167 | 4917 |
} |
| 4918 |
+ }, |
|
| 4919 |
+ "node_modules/node-addon-api": {
|
|
| 4920 |
+ "version": "7.1.1", |
|
| 4921 |
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", |
|
| 4922 |
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", |
|
| 4923 |
+ "dev": true, |
|
| 4924 |
+ "license": "MIT", |
|
| 4925 |
+ "optional": true |
|
| 5168 | 4926 |
}, |
| 5169 | 4927 |
"node_modules/node-exports-info": {
|
| 5170 | 4928 |
"version": "1.6.2", |
... | ... | @@ -5456,35 +5214,6 @@ |
| 5456 | 5214 |
"node": ">= 0.4" |
| 5457 | 5215 |
} |
| 5458 | 5216 |
}, |
| 5459 |
- "node_modules/postcss": {
|
|
| 5460 |
- "version": "8.5.23", |
|
| 5461 |
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", |
|
| 5462 |
- "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", |
|
| 5463 |
- "dev": true, |
|
| 5464 |
- "funding": [ |
|
| 5465 |
- {
|
|
| 5466 |
- "type": "opencollective", |
|
| 5467 |
- "url": "https://opencollective.com/postcss/" |
|
| 5468 |
- }, |
|
| 5469 |
- {
|
|
| 5470 |
- "type": "tidelift", |
|
| 5471 |
- "url": "https://tidelift.com/funding/github/npm/postcss" |
|
| 5472 |
- }, |
|
| 5473 |
- {
|
|
| 5474 |
- "type": "github", |
|
| 5475 |
- "url": "https://github.com/sponsors/ai" |
|
| 5476 |
- } |
|
| 5477 |
- ], |
|
| 5478 |
- "license": "MIT", |
|
| 5479 |
- "dependencies": {
|
|
| 5480 |
- "nanoid": "^3.3.16", |
|
| 5481 |
- "picocolors": "^1.1.1", |
|
| 5482 |
- "source-map-js": "^1.2.1" |
|
| 5483 |
- }, |
|
| 5484 |
- "engines": {
|
|
| 5485 |
- "node": "^10 || ^12 || >=14" |
|
| 5486 |
- } |
|
| 5487 |
- }, |
|
| 5488 | 5217 |
"node_modules/prelude-ls": {
|
| 5489 | 5218 |
"version": "1.2.1", |
| 5490 | 5219 |
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", |
... | ... | @@ -5565,6 +5294,20 @@ |
| 5565 | 5294 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", |
| 5566 | 5295 |
"dev": true, |
| 5567 | 5296 |
"license": "MIT" |
| 5297 |
+ }, |
|
| 5298 |
+ "node_modules/readdirp": {
|
|
| 5299 |
+ "version": "5.1.1", |
|
| 5300 |
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", |
|
| 5301 |
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", |
|
| 5302 |
+ "devOptional": true, |
|
| 5303 |
+ "license": "MIT", |
|
| 5304 |
+ "engines": {
|
|
| 5305 |
+ "node": ">= 20.19.0" |
|
| 5306 |
+ }, |
|
| 5307 |
+ "funding": {
|
|
| 5308 |
+ "type": "individual", |
|
| 5309 |
+ "url": "https://paulmillr.com/funding/" |
|
| 5310 |
+ } |
|
| 5568 | 5311 |
}, |
| 5569 | 5312 |
"node_modules/reflect.getprototypeof": {
|
| 5570 | 5313 |
"version": "1.0.10", |
... | ... | @@ -5742,6 +5485,27 @@ |
| 5742 | 5485 |
}, |
| 5743 | 5486 |
"funding": {
|
| 5744 | 5487 |
"url": "https://github.com/sponsors/ljharb" |
| 5488 |
+ } |
|
| 5489 |
+ }, |
|
| 5490 |
+ "node_modules/sass": {
|
|
| 5491 |
+ "version": "1.102.0", |
|
| 5492 |
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz", |
|
| 5493 |
+ "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", |
|
| 5494 |
+ "devOptional": true, |
|
| 5495 |
+ "license": "MIT", |
|
| 5496 |
+ "dependencies": {
|
|
| 5497 |
+ "chokidar": "^5.0.0", |
|
| 5498 |
+ "immutable": "^5.1.5", |
|
| 5499 |
+ "source-map-js": ">=0.6.2 <2.0.0" |
|
| 5500 |
+ }, |
|
| 5501 |
+ "bin": {
|
|
| 5502 |
+ "sass": "sass.js" |
|
| 5503 |
+ }, |
|
| 5504 |
+ "engines": {
|
|
| 5505 |
+ "node": ">=20.19.0" |
|
| 5506 |
+ }, |
|
| 5507 |
+ "optionalDependencies": {
|
|
| 5508 |
+ "@parcel/watcher": "^2.4.1" |
|
| 5745 | 5509 |
} |
| 5746 | 5510 |
}, |
| 5747 | 5511 |
"node_modules/scheduler": {
|
... | ... | @@ -6180,27 +5944,6 @@ |
| 6180 | 5944 |
}, |
| 6181 | 5945 |
"funding": {
|
| 6182 | 5946 |
"url": "https://github.com/sponsors/ljharb" |
| 6183 |
- } |
|
| 6184 |
- }, |
|
| 6185 |
- "node_modules/tailwindcss": {
|
|
| 6186 |
- "version": "4.3.3", |
|
| 6187 |
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", |
|
| 6188 |
- "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", |
|
| 6189 |
- "dev": true, |
|
| 6190 |
- "license": "MIT" |
|
| 6191 |
- }, |
|
| 6192 |
- "node_modules/tapable": {
|
|
| 6193 |
- "version": "2.3.3", |
|
| 6194 |
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", |
|
| 6195 |
- "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", |
|
| 6196 |
- "dev": true, |
|
| 6197 |
- "license": "MIT", |
|
| 6198 |
- "engines": {
|
|
| 6199 |
- "node": ">=6" |
|
| 6200 |
- }, |
|
| 6201 |
- "funding": {
|
|
| 6202 |
- "type": "opencollective", |
|
| 6203 |
- "url": "https://opencollective.com/webpack" |
|
| 6204 | 5947 |
} |
| 6205 | 5948 |
}, |
| 6206 | 5949 |
"node_modules/tinyglobby": {
|
--- package.json
+++ package.json
... | ... | @@ -14,13 +14,12 @@ |
| 14 | 14 |
"react-dom": "19.2.4" |
| 15 | 15 |
}, |
| 16 | 16 |
"devDependencies": {
|
| 17 |
- "@tailwindcss/postcss": "^4", |
|
| 18 | 17 |
"@types/node": "^20", |
| 19 | 18 |
"@types/react": "^19", |
| 20 | 19 |
"@types/react-dom": "^19", |
| 21 | 20 |
"eslint": "^9", |
| 22 | 21 |
"eslint-config-next": "16.2.12", |
| 23 |
- "tailwindcss": "^4", |
|
| 22 |
+ "sass": "^1.102.0", |
|
| 24 | 23 |
"typescript": "^5" |
| 25 | 24 |
} |
| 26 | 25 |
} |
--- postcss.config.mjs
... | ... | @@ -1,7 +0,0 @@ |
| 1 | -const config = { | |
| 2 | - plugins: { | |
| 3 | - "@tailwindcss/postcss": {}, | |
| 4 | - }, | |
| 5 | -}; | |
| 6 | - | |
| 7 | -export default config; |
--- proxy.ts
+++ proxy.ts
... | ... | @@ -16,12 +16,32 @@ |
| 16 | 16 |
*/ |
| 17 | 17 |
const PUBLIC_PATHS = ['/login']; |
| 18 | 18 |
|
| 19 |
+/** |
|
| 20 |
+ * 개발 환경에서만 인증을 면제하는 경로. 디자인 시스템 카탈로그(`/design`)는 개발 전용 |
|
| 21 |
+ * 라우트이며 프로덕션에서는 페이지 자체가 `notFound()`로 404를 낸다 — 즉 여기서 열어도 |
|
| 22 |
+ * 프로덕션 노출 경로가 생기지 않는다. 백엔드나 세션 없이도 토큰을 확인할 수 있게 한다. |
|
| 23 |
+ * |
|
| 24 |
+ * fail-closed 원칙은 유지된다: 이 목록은 개발 빌드에서만 평가되고, 프로덕션 빌드에서는 |
|
| 25 |
+ * `PUBLIC_PATHS`만 남는다. |
|
| 26 |
+ */ |
|
| 27 |
+const DEV_ONLY_PUBLIC_PATHS = ['/design']; |
|
| 28 |
+ |
|
| 29 |
+function isPublicPath(pathname: string): boolean {
|
|
| 30 |
+ if (PUBLIC_PATHS.includes(pathname)) {
|
|
| 31 |
+ return true; |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ return ( |
|
| 35 |
+ process.env.NODE_ENV !== 'production' && |
|
| 36 |
+ DEV_ONLY_PUBLIC_PATHS.includes(pathname) |
|
| 37 |
+ ); |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 19 | 40 |
export function proxy(request: NextRequest) {
|
| 20 | 41 |
const { pathname } = request.nextUrl;
|
| 21 |
- const isPublicPath = PUBLIC_PATHS.includes(pathname); |
|
| 22 | 42 |
const hasSessionCookie = request.cookies.has(SESSION_COOKIE_NAME); |
| 23 | 43 |
|
| 24 |
- if (!isPublicPath && !hasSessionCookie) {
|
|
| 44 |
+ if (!isPublicPath(pathname) && !hasSessionCookie) {
|
|
| 25 | 45 |
return NextResponse.redirect(new URL('/login', request.url));
|
| 26 | 46 |
} |
| 27 | 47 |
|
--- tsconfig.json
+++ tsconfig.json
... | ... | @@ -19,7 +19,8 @@ |
| 19 | 19 |
} |
| 20 | 20 |
], |
| 21 | 21 |
"paths": {
|
| 22 |
- "@/*": ["./*"] |
|
| 22 |
+ "@/*": ["./*"], |
|
| 23 |
+ "@fox/*": ["./@fox/*"] |
|
| 23 | 24 |
} |
| 24 | 25 |
}, |
| 25 | 26 |
"include": [ |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?