From 8929b714e6da3d98d6c40097eb422de5a58c7b78 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Thu, 9 Apr 2026 10:03:09 +0800 Subject: [PATCH] feat(playground): add new playground UI components and update RenderTree tabs --- .agent/skills/frontend-design/SKILL.md | 42 + .../src/components/Button/Button.tsx | 253 ---- .../src/components/Button/debounce.ts | 16 - .../playgrounds/src/components/ExportCom.tsx | 11 - .../playgrounds/src/components/Text/Text.tsx | 5 - .../playground/async-state-panel.tsx | 131 +++ .../playground/environment-card.tsx | 84 ++ .../src/components/playground/icons.tsx | 76 ++ .../components/playground/interaction-lab.tsx | 382 ++++++ .../src/components/playground/lab-context.ts | 21 + .../playgrounds/src/components/test-com.tsx | 6 - .../src/content/playground-content.ts | 309 +++++ .../src/content/playground-types.ts | 53 + packages/playgrounds/src/global.css | 1035 +++++++++++++++++ packages/playgrounds/src/root.tsx | 6 +- .../playgrounds/src/routes/about/index.tsx | 117 +- .../src/routes/blog/[slug]/index.tsx | 114 ++ .../playgrounds/src/routes/blog/index.tsx | 102 +- packages/playgrounds/src/routes/index.tsx | 186 ++- packages/playgrounds/src/routes/layout.tsx | 71 +- .../ui/src/features/RenderTree/RenderTree.tsx | 11 +- .../RenderTree/components/RenderTreeTabs.tsx | 9 +- 22 files changed, 2683 insertions(+), 357 deletions(-) create mode 100644 .agent/skills/frontend-design/SKILL.md delete mode 100644 packages/playgrounds/src/components/Button/Button.tsx delete mode 100644 packages/playgrounds/src/components/Button/debounce.ts delete mode 100644 packages/playgrounds/src/components/ExportCom.tsx delete mode 100644 packages/playgrounds/src/components/Text/Text.tsx create mode 100644 packages/playgrounds/src/components/playground/async-state-panel.tsx create mode 100644 packages/playgrounds/src/components/playground/environment-card.tsx create mode 100644 packages/playgrounds/src/components/playground/icons.tsx create mode 100644 packages/playgrounds/src/components/playground/interaction-lab.tsx create mode 100644 packages/playgrounds/src/components/playground/lab-context.ts delete mode 100644 packages/playgrounds/src/components/test-com.tsx create mode 100644 packages/playgrounds/src/content/playground-content.ts create mode 100644 packages/playgrounds/src/content/playground-types.ts create mode 100644 packages/playgrounds/src/routes/blog/[slug]/index.tsx diff --git a/.agent/skills/frontend-design/SKILL.md b/.agent/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..1dff30b --- /dev/null +++ b/.agent/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file diff --git a/packages/playgrounds/src/components/Button/Button.tsx b/packages/playgrounds/src/components/Button/Button.tsx deleted file mode 100644 index 2d1d7b1..0000000 --- a/packages/playgrounds/src/components/Button/Button.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - component$, - useStore, - useSignal, - useTask$, - useComputed$, - $, - useContext, - useContextProvider, - useId, - useOnDocument, - useOnWindow, - useResource$, - useStyles$, - useStylesScoped$, - createContextId, - Resource, - useAsync$, - useSerializer$, - useConstant, - useOn, - useServerData, - useErrorBoundary, -} from '@qwik.dev/core'; -import { - _getDomContainer, - isServer, - useVisibleTask$, -} from '@qwik.dev/core/internal'; -import type { QRL, Signal } from '@qwik.dev/core'; -import { - useLocation, - useNavigate, - Link, - useContent, - useDocumentHead, -} from '@qwik.dev/router'; -import { useDebouncer } from './debounce'; -const ButtonContext = createContextId<{ theme: string; size: string }>( - 'button-context', -); - -interface ButtonProps { - class?: string; - onClick$?: QRL<() => void>; - testValue: Signal; -} - -export default component$((props) => { - const { class: className = '', onClick$ } = props; - const testValue2 = props.testValue; - console.log('testValue', testValue2); - const store = useStore({ - count: 0, - dd: 12, - cc: 33, - aa: [1, 2, 3], - }); - const signal = useSignal('111'); - const constantValue = useConstant(() => 'CONST'); - const serverData = useServerData('demo-key'); - const errorBoundary = useErrorBoundary(); - const location = useLocation(); - const navigate = useNavigate(); - const content = useContent(); - const head = useDocumentHead(); - - useTask$(({ track }) => { - track(() => store.count); - signal.value = '33333'; - }); - - useVisibleTask$(({ track }) => { - track(() => store.count); - signal.value = '2227'; - }); - - const qwikContainer = useComputed$(() => { - try { - if (isServer) { - return null; - } - const htmlElement = document.documentElement; - return _getDomContainer(htmlElement); - } catch (error) { - console.error(error); - return null; - } - }); - - const asyncComputedValue = useAsync$( - async ({ track }) => Promise.resolve(track(signal) + 3), - { initial: '' }, - ); - - useContextProvider(ButtonContext, { - theme: 'primary', - size: 'large', - }); - - const context = useContext(ButtonContext); - - const buttonId = useId(); - - useOnDocument( - 'keydown', - $(() => { - console.log('Document keydown event'); - }), - ); - - useOnWindow( - 'resize', - $(() => { - console.log('Window resized'); - }), - ); - - useOn( - 'click', - $(() => { - console.log('Host clicked'); - }), - ); - - const resourceData = useResource$(async ({ track }) => { - track(() => store.count); - await new Promise((resolve) => setTimeout(resolve, 200)); - return { - message: `Resource data for count: ${store.count}`, - timestamp: Date.now(), - }; - }); - - const customSerialized = useSerializer$(() => ({ - deserialize: () => ({ n: store.count }), - update: (current: { n: number }) => { - current.n = store.count; - return current; - }, - })); - - useStyles$(` - .custom-button { - background: linear-gradient(45deg, #ff6b6b, #4ecdc4); - color: white; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - transition: transform 0.2s; - } - .custom-button:hover { - transform: scale(1.05); - } - `); - - useStylesScoped$(` - .scoped-button { - background: linear-gradient(45deg, #667eea, #764ba2); - color: white; - border: none; - padding: 8px 16px; - border-radius: 3px; - cursor: pointer; - } - `); - const debounce = useDebouncer( - $((value: string) => { - signal.value = value; - }), - 1000, - ); - - useDebouncer( - $((value: string) => { - signal.value = value; - }), - 1000, - ); - - const handleClick = $(async () => { - store.count++; - console.log('Button clicked! Count:', store.count); - debounce(store.count); - if (onClick$) { - await onClick$(); - } - }); - - const handleGoAbout = $(() => navigate('/about')); - - return ( -
- - - - Go /blog - - -
-
Current Path: {location.url.pathname}
-
Is Navigating: {location.isNavigating ? 'true' : 'false'}
-
Params: {JSON.stringify(location.params)}
-
- Prev URL: {location.prevUrl ? location.prevUrl.pathname : '—'} -
-
Head Title: {head.title}
-
Head Metas: {head.meta.length}
-
Content Menu: {content.menu ? 'yes' : 'no'}
-
- Content Headings: {content.headings ? content.headings.length : 0} -
-
Async Computed: {asyncComputedValue.value}
-
- Context: {context.theme} - {context.size} -
-
Button ID: {buttonId}
-
Constant: {constantValue}
- {errorBoundary.error &&
Error captured
} -
ServerData: {serverData ? JSON.stringify(serverData) : 'N/A'}
-
Serialized N: {customSerialized.value.n}
-
Loading resource...
} - onResolved={(data) =>
Resource: {data.message}
} - onRejected={(error) =>
Error: {error.message}
} - /> -
- Count: {store.count}, Signal: {signal.value} -
-
-
- ); -}); diff --git a/packages/playgrounds/src/components/Button/debounce.ts b/packages/playgrounds/src/components/Button/debounce.ts deleted file mode 100644 index a577ed1..0000000 --- a/packages/playgrounds/src/components/Button/debounce.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { QRL, useSignal, $, useTask$ } from '@qwik.dev/core'; - -export const useDebouncer = (fn: QRL<(args: any) => void>, delay: number) => { - const timeoutId = useSignal(11); - const timeout = useSignal(11); - useTask$(({ track }) => { - track(() => timeout.value); - }); - return $((args: any) => { - clearTimeout(timeoutId.value); - console.log('timeout', timeout.value); - timeoutId.value = Number(setTimeout(() => fn(args), delay)); - }); -}; - -// export const useDebouncer = implicit$FirstArg(useDebouncerx); diff --git a/packages/playgrounds/src/components/ExportCom.tsx b/packages/playgrounds/src/components/ExportCom.tsx deleted file mode 100644 index 0bd88f6..0000000 --- a/packages/playgrounds/src/components/ExportCom.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { component$, useSignal } from '@qwik.dev/core'; - -export const ExportCom = component$(() => { - const signal = useSignal(0); - return
11{signal.value}
; -}); - -export const ExportCom2 = component$(() => { - const signal = useSignal(1); - return
11{signal.value}
; -}); diff --git a/packages/playgrounds/src/components/Text/Text.tsx b/packages/playgrounds/src/components/Text/Text.tsx deleted file mode 100644 index 795db81..0000000 --- a/packages/playgrounds/src/components/Text/Text.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { component$ } from '@qwik.dev/core'; - -export default component$(() => { - return

Hello World

; -}); diff --git a/packages/playgrounds/src/components/playground/async-state-panel.tsx b/packages/playgrounds/src/components/playground/async-state-panel.tsx new file mode 100644 index 0000000..47df20c --- /dev/null +++ b/packages/playgrounds/src/components/playground/async-state-panel.tsx @@ -0,0 +1,131 @@ +import { Resource, component$, useComputed$, useContext, useResource$ } from '@qwik.dev/core'; +import { experimentPresets } from '~/content/playground-content'; +import { LabContext } from './lab-context'; +import { PlaygroundGlyph } from './icons'; + +interface SyntheticPayload { + phase: 'resolved' | 'empty'; + requestId: string; + summary: string; + sample: string; + edges: number; + latencyMs: number; +} + +export const AsyncStatePanel = component$(() => { + const lab = useContext(LabContext); + + const activePreset = useComputed$( + () => + experimentPresets.find((preset) => preset.id === lab.presetId.value) ?? + experimentPresets[1], + ); + + const snapshot = useResource$(async ({ track }) => { + const presetId = track(() => lab.presetId.value); + const dataMode = track(() => lab.dataMode.value); + const interactions = track(() => lab.interactionCount.value); + const requestCycle = track(() => lab.requestCycle.value); + const preset = + experimentPresets.find((item) => item.id === presetId) ?? + experimentPresets[1]; + + await new Promise((resolve) => setTimeout(resolve, preset.latencyMs)); + + if (dataMode === 'error') { + throw new Error( + `Synthetic failure injected on cycle ${requestCycle + 1} under ${preset.label}.`, + ); + } + + if (dataMode === 'empty') { + return { + phase: 'empty', + requestId: `lab-${requestCycle + 1}`, + summary: 'No payload returned for the current scenario filter.', + sample: 'Try switching to resolved or change the latency preset.', + edges: 0, + latencyMs: preset.latencyMs, + }; + } + + return { + phase: 'resolved', + requestId: `lab-${requestCycle + 1}`, + summary: `${preset.label} completed with a stable synthetic payload.`, + sample: `${interactions * 4} signal edges and route-ready metadata were packed into the current response.`, + edges: interactions * 4, + latencyMs: preset.latencyMs, + }; + }); + + return ( +
+
+
+
Async State Panel
+

Resource lifecycle with visible pending, empty, and failure modes.

+
+
+ + {activePreset.value.label} +
+
+ + ( +
+
+
+
Awaiting synthetic payload...
+
+ Holding for {activePreset.value.latencyMs}ms to make the pending + state inspectable in devtools. +
+
+
+ )} + onRejected={(error) => ( +
+
Failure branch
+
{error.message}
+
+ The UI footprint stays stable so that failure remains visually + comparable to resolved states. +
+
+ )} + onResolved={(payload) => ( +
+
+ {payload.phase === 'empty' ? 'Empty branch' : 'Resolved branch'} +
+
{payload.summary}
+
{payload.sample}
+
+
+ Request + {payload.requestId} +
+
+ Edges + {payload.edges} +
+
+ Latency + {payload.latencyMs}ms +
+
+
+ )} + /> +
+ ); +}); diff --git a/packages/playgrounds/src/components/playground/environment-card.tsx b/packages/playgrounds/src/components/playground/environment-card.tsx new file mode 100644 index 0000000..7909818 --- /dev/null +++ b/packages/playgrounds/src/components/playground/environment-card.tsx @@ -0,0 +1,84 @@ +import { component$, useComputed$, useContext, type Signal } from '@qwik.dev/core'; +import { useLocation } from '@qwik.dev/router'; +import { experimentPresets } from '~/content/playground-content'; +import type { LabEvent } from '~/content/playground-types'; +import { LabContext } from './lab-context'; +import { PlaygroundGlyph } from './icons'; + +interface EnvironmentCardProps { + serverTime: string; + eventLog: Signal; +} + +export const EnvironmentCard = component$( + ({ serverTime, eventLog }) => { + const lab = useContext(LabContext); + const location = useLocation(); + + const activePreset = useComputed$( + () => + experimentPresets.find((preset) => preset.id === lab.presetId.value) ?? + experimentPresets[1], + ); + + return ( +
+
+
+
Environment Card
+

Route, context, and event signals compressed into one readable panel.

+
+
+ + {location.url.pathname} +
+
+ +
+
+ Server clock + {serverTime} +
+
+ Previous route + {location.prevUrl?.pathname ?? 'First stop'} +
+
+ Data mode + {lab.dataMode.value} +
+
+ Layout mode + {lab.layoutMode.value} +
+
+ Motion density + {lab.motionDensity.value} +
+
+ Preset tone + {activePreset.value.tone} +
+
+ +
+
+ Recent events + {lab.lastAction.value} +
+
+ {eventLog.value.map((event) => ( +
+
+ {event.time} + {event.label} +
+
{event.detail}
+
+ ))} +
+
+
+ ); + }, +); diff --git a/packages/playgrounds/src/components/playground/icons.tsx b/packages/playgrounds/src/components/playground/icons.tsx new file mode 100644 index 0000000..794950c --- /dev/null +++ b/packages/playgrounds/src/components/playground/icons.tsx @@ -0,0 +1,76 @@ +import { component$ } from '@qwik.dev/core'; + +type PlaygroundGlyphName = + | 'spark' + | 'route' + | 'stack' + | 'pulse' + | 'notes' + | 'arrow-up-right'; + +interface PlaygroundGlyphProps { + name: PlaygroundGlyphName; + class?: string; +} + +export const PlaygroundGlyph = component$( + ({ name, class: className }) => { + const shared = { + fill: 'none', + stroke: 'currentColor', + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, + strokeWidth: 1.7, + }; + + return ( + + ); + }, +); diff --git a/packages/playgrounds/src/components/playground/interaction-lab.tsx b/packages/playgrounds/src/components/playground/interaction-lab.tsx new file mode 100644 index 0000000..2ff9f88 --- /dev/null +++ b/packages/playgrounds/src/components/playground/interaction-lab.tsx @@ -0,0 +1,382 @@ +import { + $, + component$, + useComputed$, + useContextProvider, + useSignal, +} from '@qwik.dev/core'; +import { useNavigate } from '@qwik.dev/router'; +import { + experimentPresets, + initialLabEvents, +} from '~/content/playground-content'; +import type { LabEvent } from '~/content/playground-types'; +import { AsyncStatePanel } from './async-state-panel'; +import { EnvironmentCard } from './environment-card'; +import { + LabContext, + type DataMode, + type LayoutMode, + type MotionDensity, + type SurfaceIntensity, +} from './lab-context'; +import { PlaygroundGlyph } from './icons'; + +interface InteractionLabProps { + serverTime: string; +} + +function createLabEvent( + label: string, + detail: string, + tone: LabEvent['tone'] = 'info', +): LabEvent { + return { + id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + label, + detail, + time: new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(new Date()), + tone, + }; +} + +export const InteractionLab = component$(({ serverTime }) => { + const navigate = useNavigate(); + const presetId = useSignal(experimentPresets[1].id); + const dataMode = useSignal('resolved'); + const motionDensity = useSignal('elevated'); + const layoutMode = useSignal('grid'); + const surfaceIntensity = useSignal('balanced'); + const interactionCount = useSignal(4); + const requestCycle = useSignal(0); + const lastAction = useSignal('Ready to inspect live state.'); + const eventLog = useSignal(initialLabEvents.map((event) => ({ ...event }))); + + useContextProvider(LabContext, { + presetId, + dataMode, + motionDensity, + layoutMode, + surfaceIntensity, + interactionCount, + requestCycle, + lastAction, + }); + + const activePreset = useComputed$( + () => + experimentPresets.find((preset) => preset.id === presetId.value) ?? + experimentPresets[1], + ); + + const activityIndex = useComputed$(() => { + const intensityBoost = + surfaceIntensity.value === 'surge' + ? 22 + : surfaceIntensity.value === 'balanced' + ? 14 + : 8; + const dataBoost = dataMode.value === 'error' ? 11 : dataMode.value === 'empty' ? 6 : 15; + return interactionCount.value * 9 + intensityBoost + dataBoost; + }); + + const applyPreset = $((id: string) => { + const preset = experimentPresets.find((item) => item.id === id); + if (!preset) { + return; + } + + presetId.value = id; + requestCycle.value++; + lastAction.value = `${preset.label} preset armed.`; + eventLog.value = [ + createLabEvent( + 'Preset updated', + `${preset.label} now drives synthetic latency at ${preset.latencyMs}ms.`, + 'success', + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const applyDataMode = $((nextMode: DataMode) => { + dataMode.value = nextMode; + requestCycle.value++; + lastAction.value = `Scenario switched to ${nextMode}.`; + eventLog.value = [ + createLabEvent( + 'Scenario changed', + `The resource panel is now rendering the ${nextMode} branch.`, + nextMode === 'error' ? 'warning' : 'info', + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const applyMotion = $((nextMotion: MotionDensity) => { + motionDensity.value = nextMotion; + lastAction.value = `Motion set to ${nextMotion}.`; + eventLog.value = [ + createLabEvent( + 'Motion density', + `${nextMotion} motion keeps the stage transitions deliberate.`, + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const applyLayout = $((nextLayout: LayoutMode) => { + layoutMode.value = nextLayout; + lastAction.value = `Layout switched to ${nextLayout}.`; + eventLog.value = [ + createLabEvent( + 'Layout updated', + `${nextLayout} mode rearranged the experiment surface.`, + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const applySurface = $((nextSurface: SurfaceIntensity) => { + surfaceIntensity.value = nextSurface; + lastAction.value = `Surface intensity set to ${nextSurface}.`; + eventLog.value = [ + createLabEvent( + 'Surface tuned', + `${nextSurface} intensity changed the overall command-center mood.`, + 'success', + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const nudgeInteraction = $((delta: number) => { + interactionCount.value = Math.max(1, interactionCount.value + delta); + requestCycle.value++; + lastAction.value = `Interaction volume is now ${interactionCount.value}.`; + eventLog.value = [ + createLabEvent( + 'Interaction count', + `Signal load adjusted to ${interactionCount.value} interactions.`, + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const triggerAsync = $(() => { + requestCycle.value++; + lastAction.value = 'Synthetic resource refresh triggered.'; + eventLog.value = [ + createLabEvent( + 'Async refresh', + `Queued request cycle ${requestCycle.value + 1} for the active preset.`, + 'success', + ), + ...eventLog.value, + ].slice(0, 6); + }); + + const simulateNavigation = $(async () => { + const target = requestCycle.value % 2 === 0 ? '/about' : '/blog'; + lastAction.value = `Navigating to ${target}.`; + eventLog.value = [ + createLabEvent( + 'Route jump', + `Quick action opened ${target} to test cross-route continuity.`, + 'warning', + ), + ...eventLog.value, + ].slice(0, 6); + await navigate(target); + }); + + const resetLab = $(() => { + presetId.value = experimentPresets[1].id; + dataMode.value = 'resolved'; + motionDensity.value = 'elevated'; + layoutMode.value = 'grid'; + surfaceIntensity.value = 'balanced'; + interactionCount.value = 4; + requestCycle.value++; + lastAction.value = 'Lab reset to the default observation profile.'; + eventLog.value = [ + createLabEvent( + 'Lab reset', + 'Default observation profile restored for a clean pass.', + 'success', + ), + ...initialLabEvents.slice(0, 2).map((event) => ({ ...event })), + ]; + }); + + return ( +
+
+
+
+
Live Experiment Panel
+

Controls inspired by modern playgrounds, tuned for Qwik state visibility.

+
+
+ + {activePreset.value.label} +
+
+ +
+
+ Activity index + {activityIndex.value} +
+
+ Latency + {activePreset.value.latencyMs}ms +
+
+ Cycles queued + {requestCycle.value + 1} +
+
+ +
+
+
Latency preset
+
+ {experimentPresets.map((preset) => ( + + ))} +
+
+ +
+
Data state
+
+ {(['resolved', 'empty', 'error'] as const).map((mode) => ( + + ))} +
+
+ +
+
Surface intensity
+
+ {(['soft', 'balanced', 'surge'] as const).map((surface) => ( + + ))} +
+
+ +
+
Motion density
+
+ {(['elevated', 'calm'] as const).map((motion) => ( + + ))} +
+
+ +
+
Card layout
+
+ {(['grid', 'stack'] as const).map((layout) => ( + + ))} +
+
+ +
+
Signal volume
+
+ + {interactionCount.value} + +
+
+
+ +
+ + + +
+
+ +
+ + +
+
+ ); +}); diff --git a/packages/playgrounds/src/components/playground/lab-context.ts b/packages/playgrounds/src/components/playground/lab-context.ts new file mode 100644 index 0000000..fbfcc41 --- /dev/null +++ b/packages/playgrounds/src/components/playground/lab-context.ts @@ -0,0 +1,21 @@ +import { createContextId, type Signal } from '@qwik.dev/core'; + +export type DataMode = 'resolved' | 'empty' | 'error'; +export type MotionDensity = 'calm' | 'elevated'; +export type LayoutMode = 'grid' | 'stack'; +export type SurfaceIntensity = 'soft' | 'balanced' | 'surge'; + +export interface LabContextValue { + presetId: Signal; + dataMode: Signal; + motionDensity: Signal; + layoutMode: Signal; + surfaceIntensity: Signal; + interactionCount: Signal; + requestCycle: Signal; + lastAction: Signal; +} + +export const LabContext = createContextId( + 'playground.lab-context', +); diff --git a/packages/playgrounds/src/components/test-com.tsx b/packages/playgrounds/src/components/test-com.tsx deleted file mode 100644 index 9127b98..0000000 --- a/packages/playgrounds/src/components/test-com.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { component$, useSignal } from '@qwik.dev/core'; - -export default component$(() => { - const signal = useSignal(1); - return
11{signal.value}
; -}); diff --git a/packages/playgrounds/src/content/playground-content.ts b/packages/playgrounds/src/content/playground-content.ts new file mode 100644 index 0000000..9356903 --- /dev/null +++ b/packages/playgrounds/src/content/playground-content.ts @@ -0,0 +1,309 @@ +import type { + BlogEntry, + ExperimentPreset, + FeatureCard, + LabEvent, + ShowcaseMetric, +} from './playground-types'; + +export const siteNavigation = [ + { href: '/', label: 'Command Center' }, + { href: '/about', label: 'Architecture' }, + { href: '/blog', label: 'Lab Notes' }, +] as const; + +export const homeMetrics: ShowcaseMetric[] = [ + { + label: 'Route Surfaces', + value: '4', + detail: 'Home, architecture, notes, and one nested article route.', + }, + { + label: 'Observation Modes', + value: '6', + detail: 'Routing, state, resource, events, context, and devtools visibility.', + }, + { + label: 'Latency Presets', + value: '3', + detail: 'Switch among calm, balanced, and surge behavior profiles.', + }, +]; + +export const featureCards: FeatureCard[] = [ + { + id: 'routing', + eyebrow: 'Routing Surface', + title: 'Directory routes feel like product structure, not scaffolding.', + description: + 'The playground treats routes as narrative beats: command center, architecture, notes, and detail.', + points: ['Shared shell', 'Nested note route', 'Location-aware navigation'], + accent: 'cyan', + }, + { + id: 'resumability', + eyebrow: 'Resumability', + title: 'Signals stay lightweight until an interaction actually matters.', + description: + 'The interface exposes Qwik state without turning the page into a debugger dump.', + points: ['Signal counters', 'Context-fed cards', 'Low-friction hydration hints'], + accent: 'lime', + }, + { + id: 'resources', + eyebrow: 'Async Data', + title: 'Pending, resolved, empty, and failure states are first-class citizens.', + description: + 'A synthetic resource panel makes latency and state transitions visible at a glance.', + points: ['Preset latency', 'Scenario toggles', 'Resource lifecycle output'], + accent: 'amber', + }, + { + id: 'events', + eyebrow: 'Interaction Design', + title: 'Quick actions mimic the control strips people expect from modern playgrounds.', + description: + 'Preset buttons, scenario switches, and resets give the demo a repeatable rhythm.', + points: ['Scenario shortcuts', 'Reset actions', 'Readable event timeline'], + accent: 'violet', + }, + { + id: 'state', + eyebrow: 'State Graph', + title: 'Store-derived metrics and contextual panels turn raw state into meaning.', + description: + 'Instead of showing everything, the demo groups state into system health, activity, and payload readiness.', + points: ['Derived metrics', 'Signal-to-copy mapping', 'Event compression'], + accent: 'cyan', + }, + { + id: 'visibility', + eyebrow: 'Devtools Visibility', + title: 'This app is intentionally rich enough to give Qwik Devtools something real to inspect.', + description: + 'The shell, routes, resources, and controls all create a more representative observation target.', + points: ['Plugin-ready shell', 'Route-rich flows', 'Live state for inspection'], + accent: 'lime', + }, +]; + +export const experimentPresets: ExperimentPreset[] = [ + { + id: 'soft-glow', + label: 'Soft Glow', + latencyMs: 220, + intensity: 'soft', + layout: 'grid', + tone: 'Quiet, precise, and good for route checks.', + description: 'Fast feedback with restrained motion and quick resource resolution.', + }, + { + id: 'signal-mix', + label: 'Signal Mix', + latencyMs: 680, + intensity: 'balanced', + layout: 'grid', + tone: 'Balanced for state tracking and async visibility.', + description: 'The default profile for observing signals, derived metrics, and pending states.', + }, + { + id: 'surge-mode', + label: 'Surge Mode', + latencyMs: 1200, + intensity: 'surge', + layout: 'stack', + tone: 'High contrast, longer waits, and more dramatic transitions.', + description: 'Useful when you want to stress the resource panel and route jumps.', + }, +]; + +export const initialLabEvents: LabEvent[] = [ + { + id: 'boot', + label: 'Shell online', + detail: 'The command center mounted with a shared route shell.', + time: 'Boot', + tone: 'success', + }, + { + id: 'resource', + label: 'Resource channel warm', + detail: 'Async panel is ready to simulate pending and failure states.', + time: 'Async', + tone: 'info', + }, + { + id: 'devtools', + label: 'Inspection ready', + detail: 'The page now exposes enough activity to be useful in devtools.', + time: 'Observe', + tone: 'warning', + }, +]; + +export const aboutPrinciples = [ + { + title: 'Route shape should teach the product.', + body: + 'Each route demonstrates a different information mode: overview, rules, list, and long-form detail. This makes the demo useful for routing checks instead of only screenshot polish.', + }, + { + title: 'State needs narrative, not raw dumps.', + body: + 'Signals, derived values, resource lifecycle, and navigation state are grouped into compact cards so the UI reads like a system console instead of a debug transcript.', + }, + { + title: 'Async behavior deserves deliberate stagecraft.', + body: + 'Latency presets and scenario controls let people see pending, empty, and failure modes without wiring a real backend.', + }, + { + title: 'Design language should echo the tool it supports.', + body: + 'The palette borrows from the devtools package, but the playground adds its own darker, more atmospheric shell so the app feels intentional rather than copied.', + }, +] as const; + +export const visualSystemRules = [ + { + title: 'Glass surfaces, not flat boxes', + detail: 'Use layered panels, transparent borders, and soft glow instead of plain cards.', + }, + { + title: 'English copy with technical cadence', + detail: 'Keep content readable for an open-source audience while preserving product voice.', + }, + { + title: 'Data-led ornament', + detail: 'Decorative lines and meters should reinforce state, routes, or resource changes.', + }, + { + title: 'CSS-first motion', + detail: 'Prefer staged transitions and hover depth over JS-only decorative effects.', + }, +] as const; + +export const quickJumpCards = [ + { + href: '/about', + title: 'Read the architecture notes', + description: + 'See the system rules, visual grammar, and why each route exists in the demo.', + }, + { + href: '/blog', + title: 'Browse lab notes', + description: + 'Open mock experiments with tags, reading time, and one nested article route.', + }, + { + href: '/blog/latency-without-chaos', + title: 'Open the long-form article', + description: + 'Jump straight into a detailed note to verify nested routing, article layout, and code block styling.', + }, +] as const; + +export const blogEntries: BlogEntry[] = [ + { + slug: 'latency-without-chaos', + title: 'Latency Without Chaos', + summary: 'How we simulate async variance without making the playground feel brittle.', + excerpt: + 'The best playgrounds let people rehearse failure and waiting states with almost no setup. This article explains how the new async panel keeps those states visible but legible.', + readTime: '6 min read', + status: 'Fresh', + publishedAt: 'April 8, 2026', + tags: ['Async', 'UX', 'State'], + category: 'Field Study', + heroMetric: '3 preset lanes', + sections: [ + { + heading: 'Why the old demo fell apart under real interaction', + paragraphs: [ + 'The original playground exposed many hooks at once, but it did not teach people what to look at. That meant the demo was technically rich yet narratively thin.', + 'We restructured the async story around a small number of repeatable scenarios. Now pending, empty, and error are intentional states instead of incidental byproducts.', + ], + quote: + 'A good playground does not only prove that things work. It proves that waiting, failing, and recovering still feel coherent.', + }, + { + heading: 'Preset-driven latency keeps the experiment legible', + paragraphs: [ + 'Each preset establishes a recognizable rhythm. Soft Glow is nearly immediate, Signal Mix holds long enough to show pending UI, and Surge Mode exaggerates delay for inspection.', + 'Because the panel is deterministic, people can use it as a stable test surface while still feeling like they are interacting with something alive.', + ], + code: String.raw`const preset = experimentPresets.find( + (item) => item.id === presetId.value, +); + +await new Promise((resolve) => { + setTimeout(resolve, preset?.latencyMs ?? 600); +});`, + }, + { + heading: 'Readable failure is a design feature', + paragraphs: [ + 'Error states use the same panel footprint as success states. That keeps the geometry stable and makes the change feel like a mode shift rather than a collapse.', + 'In a tooling playground, that consistency matters because devtools screenshots and route transitions stay comparable across runs.', + ], + }, + ], + }, + { + slug: 'routing-as-story-architecture', + title: 'Routing as Story Architecture', + summary: 'A tiny route tree can still feel like a product if each page carries a different informational role.', + excerpt: + 'Instead of shipping three shallow pages, the new structure uses overview, rules, list, and detail patterns to make the route graph teach itself.', + readTime: '5 min read', + status: 'Stable', + publishedAt: 'April 5, 2026', + tags: ['Routing', 'Content'], + category: 'System Note', + heroMetric: '4 route surfaces', + sections: [ + { + heading: 'Structure is part of the demo', + paragraphs: [ + 'Routes tell collaborators what kinds of states the app intends to support. A demo with one page and many cards cannot teach nested content or navigation rhythm.', + ], + }, + { + heading: 'Each page gets a distinct job', + paragraphs: [ + 'The home route acts like a command surface, the about page explains principles, and blog routes provide realistic list-detail navigation.', + ], + }, + ], + }, + { + slug: 'glass-panels-with-purpose', + title: 'Glass Panels With Purpose', + summary: 'A note on using translucent layers without turning the whole UI into a blur experiment.', + excerpt: + 'Glassmorphism can quickly become visual fog. The updated playground keeps the effect concentrated in the places where hierarchy and focus benefit from it.', + readTime: '4 min read', + status: 'Field Note', + publishedAt: 'April 2, 2026', + tags: ['Visual System', 'CSS'], + category: 'Design Memo', + heroMetric: '5 surface tiers', + sections: [ + { + heading: 'Atmosphere needs contrast', + paragraphs: [ + 'Transparency only works when the background, border, and text contrast are tuned together. The playground uses deep navy fields and sharp cyan accents so the panels stay readable.', + ], + }, + ], + }, +]; + +export const blogTags = ['Async', 'CSS', 'Content', 'Routing', 'State', 'UX']; + +export const featuredBlogEntry = blogEntries[0]; + +export function getBlogEntryBySlug(slug: string) { + return blogEntries.find((entry) => entry.slug === slug); +} diff --git a/packages/playgrounds/src/content/playground-types.ts b/packages/playgrounds/src/content/playground-types.ts new file mode 100644 index 0000000..8f94cf6 --- /dev/null +++ b/packages/playgrounds/src/content/playground-types.ts @@ -0,0 +1,53 @@ +export interface ShowcaseMetric { + label: string; + value: string; + detail: string; +} + +export interface FeatureCard { + id: string; + eyebrow: string; + title: string; + description: string; + points: string[]; + accent: 'cyan' | 'lime' | 'amber' | 'violet'; +} + +export interface ExperimentPreset { + id: string; + label: string; + latencyMs: number; + intensity: 'soft' | 'balanced' | 'surge'; + layout: 'grid' | 'stack'; + tone: string; + description: string; +} + +export interface LabEvent { + id: string; + label: string; + detail: string; + time: string; + tone: 'info' | 'success' | 'warning'; +} + +export interface BlogEntrySection { + heading: string; + paragraphs: string[]; + quote?: string; + code?: string; +} + +export interface BlogEntry { + slug: string; + title: string; + summary: string; + excerpt: string; + readTime: string; + status: 'Fresh' | 'Stable' | 'Field Note'; + publishedAt: string; + tags: string[]; + category: string; + heroMetric: string; + sections: BlogEntrySection[]; +} diff --git a/packages/playgrounds/src/global.css b/packages/playgrounds/src/global.css index e69de29..438906f 100644 --- a/packages/playgrounds/src/global.css +++ b/packages/playgrounds/src/global.css @@ -0,0 +1,1035 @@ +:root { + color-scheme: dark; + --pg-font-display: + 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', 'Georgia', serif; + --pg-font-body: + 'Avenir Next', 'Segoe UI', 'Helvetica Neue', 'Arial Nova', sans-serif; + --pg-bg: #07111f; + --pg-bg-elevated: rgba(15, 26, 44, 0.72); + --pg-bg-strong: rgba(9, 17, 31, 0.9); + --pg-surface: rgba(15, 24, 42, 0.74); + --pg-surface-soft: rgba(16, 28, 47, 0.58); + --pg-border: rgba(119, 171, 255, 0.14); + --pg-border-strong: rgba(120, 196, 255, 0.28); + --pg-text: #ecf6ff; + --pg-muted: rgba(207, 224, 244, 0.76); + --pg-muted-strong: rgba(225, 236, 249, 0.92); + --pg-cyan: var(--color-primary, #16b6f6); + --pg-cyan-soft: rgba(22, 182, 246, 0.18); + --pg-lime: #79f7c3; + --pg-amber: #ffc46c; + --pg-violet: #b9a3ff; + --pg-danger: #ff7d93; + --pg-success: #79f7c3; + --pg-shadow: 0 30px 90px rgba(0, 0, 0, 0.35); + --pg-radius-xl: 28px; + --pg-radius-lg: 22px; + --pg-radius-md: 16px; + --pg-radius-sm: 12px; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; +} + +body.playground-body { + margin: 0; + font-family: var(--pg-font-body); + color: var(--pg-text); + background: + radial-gradient(circle at top left, rgba(22, 182, 246, 0.18), transparent 28%), + radial-gradient(circle at 85% 12%, rgba(121, 247, 195, 0.12), transparent 24%), + radial-gradient(circle at 50% 80%, rgba(185, 163, 255, 0.08), transparent 28%), + linear-gradient(180deg, #09111e 0%, #060b14 100%); + letter-spacing: 0.01em; +} + +body.playground-body a { + color: inherit; + text-decoration: none; +} + +body.playground-body button, +body.playground-body input, +body.playground-body textarea, +body.playground-body select { + font: inherit; +} + +.app-shell { + position: relative; + min-height: 100vh; + overflow: hidden; +} + +.app-shell__orb { + position: fixed; + inset: auto; + z-index: 0; + border-radius: 999px; + filter: blur(24px); + pointer-events: none; +} + +.app-shell__orb--one { + top: 80px; + left: -60px; + width: 240px; + height: 240px; + background: rgba(22, 182, 246, 0.18); +} + +.app-shell__orb--two { + right: -80px; + bottom: 120px; + width: 280px; + height: 280px; + background: rgba(121, 247, 195, 0.12); +} + +.app-frame { + position: relative; + z-index: 1; + width: min(1200px, calc(100% - 32px)); + margin: 0 auto; + padding: 24px 0 48px; +} + +.glass-frame, +.section-card, +.capability-card, +.principle-card, +.article-card, +.lab-panel { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 24%), + var(--pg-surface); + border: 1px solid var(--pg-border); + box-shadow: var(--pg-shadow); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} + +.topbar { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 18px; + padding: 18px 22px; + border-radius: var(--pg-radius-xl); +} + +.brand-mark { + display: inline-flex; + align-items: center; + gap: 14px; +} + +.brand-mark strong { + display: block; + font-size: 1rem; + font-weight: 700; +} + +.brand-mark small { + display: block; + margin-top: 3px; + color: var(--pg-muted); + font-size: 0.82rem; +} + +.brand-mark__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 16px; + background: linear-gradient(135deg, rgba(22, 182, 246, 0.22), rgba(121, 247, 195, 0.16)); + border: 1px solid rgba(121, 201, 255, 0.2); +} + +.brand-mark__glyph, +.panel-chip__icon, +.featured-article__icon, +.inspection-row__icon, +.jump-card__glyph, +.article-link__icon, +.capability-card__glyph { + width: 20px; + height: 20px; +} + +.topnav { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 10px; +} + +.topnav-link { + padding: 10px 14px; + border-radius: 999px; + color: var(--pg-muted); + border: 1px solid transparent; + transition: + color 180ms ease, + border-color 180ms ease, + background-color 180ms ease, + transform 180ms ease; +} + +.topnav-link:hover, +.topnav-link:focus-visible { + color: var(--pg-muted-strong); + border-color: rgba(121, 201, 255, 0.14); + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); +} + +.topnav-link--active { + color: #fff; + background: rgba(22, 182, 246, 0.18); + border-color: rgba(121, 201, 255, 0.25); +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 11px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.05); + color: var(--pg-muted-strong); + border: 1px solid rgba(121, 201, 255, 0.16); + font-size: 0.9rem; +} + +.status-pill__dot { + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--pg-success); + box-shadow: 0 0 0 7px rgba(121, 247, 195, 0.14); +} + +.page-frame { + padding-top: 22px; +} + +.page-content { + display: grid; + gap: 24px; +} + +.section-card, +.page-hero, +.featured-article, +.article-shell { + border-radius: var(--pg-radius-xl); + padding: 28px; +} + +.page-hero { + padding: 34px 30px; +} + +.section-heading { + max-width: 780px; + margin-bottom: 24px; +} + +.eyebrow-chip, +.feature-eyebrow, +.panel-eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; + color: rgba(177, 225, 255, 0.96); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.72rem; + font-weight: 700; +} + +.eyebrow-chip { + padding: 8px 12px; + border-radius: 999px; + background: rgba(22, 182, 246, 0.12); + border: 1px solid rgba(121, 201, 255, 0.18); +} + +h1, +h2, +h3 { + margin: 0; + font-family: var(--pg-font-display); + line-height: 1.02; + letter-spacing: -0.02em; +} + +h1 { + font-size: clamp(2.7rem, 6vw, 5rem); +} + +h2 { + font-size: clamp(2rem, 4vw, 3rem); +} + +h3 { + font-size: clamp(1.2rem, 2vw, 1.6rem); +} + +p { + margin: 0; + color: var(--pg-muted); + line-height: 1.75; +} + +.hero-layout { + display: grid; + grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.85fr); + gap: 22px; + padding: 0; + background: none; + border: 0; + box-shadow: none; +} + +.hero-copy, +.hero-sidebar { + border-radius: var(--pg-radius-xl); +} + +.hero-copy { + padding: 34px; +} + +.hero-lead { + max-width: 760px; + margin-top: 18px; + font-size: 1.04rem; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 24px; +} + +.hero-button, +.action-button, +.article-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 12px 18px; + border-radius: 999px; + border: 1px solid rgba(121, 201, 255, 0.18); + background: rgba(255, 255, 255, 0.03); + color: #fff; + transition: + transform 180ms ease, + background-color 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.hero-button:hover, +.hero-button:focus-visible, +.action-button:hover, +.action-button:focus-visible, +.article-link:hover, +.article-link:focus-visible, +.jump-card:hover, +.jump-card:focus-visible { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.06); + border-color: rgba(121, 201, 255, 0.32); +} + +.hero-button--primary, +.action-button--primary { + background: linear-gradient(135deg, rgba(22, 182, 246, 0.86), rgba(90, 247, 212, 0.84)); + color: #06131f; + border-color: transparent; + box-shadow: 0 22px 44px rgba(22, 182, 246, 0.24); +} + +.hero-status-grid, +.metric-strip, +.environment-grid, +.resource-stats { + display: grid; + gap: 12px; +} + +.hero-status-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 26px; +} + +.hero-status-card, +.metric-card, +.environment-item { + padding: 16px; + border-radius: var(--pg-radius-md); + border: 1px solid rgba(121, 201, 255, 0.12); + background: rgba(255, 255, 255, 0.03); +} + +.hero-status-card span, +.metric-card span, +.environment-item span, +.resource-stats span { + display: block; + color: var(--pg-muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.hero-status-card strong, +.metric-card strong, +.environment-item strong, +.resource-stats strong { + display: block; + margin-top: 8px; + color: #fff; + font-size: 1rem; +} + +.hero-sidebar { + display: grid; + gap: 16px; +} + +.metric-tower { + padding: 22px; + border-radius: var(--pg-radius-xl); +} + +.metric-tower span { + display: block; + color: var(--pg-muted); + font-size: 0.84rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.metric-tower strong { + display: block; + margin: 12px 0 10px; + font-family: var(--pg-font-display); + font-size: 2.6rem; +} + +.metric-tower p { + font-size: 0.94rem; +} + +.feature-grid, +.principles-grid, +.article-grid, +.jump-grid, +.lab-grid, +.split-section { + display: grid; + gap: 16px; +} + +.feature-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.capability-card { + padding: 24px; + border-radius: var(--pg-radius-lg); +} + +.capability-card__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.capability-card__icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + width: 46px; + height: 46px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.06); +} + +.capability-card--cyan .capability-card__icon { + color: var(--pg-cyan); +} + +.capability-card--lime .capability-card__icon { + color: var(--pg-lime); +} + +.capability-card--amber .capability-card__icon { + color: var(--pg-amber); +} + +.capability-card--violet .capability-card__icon { + color: var(--pg-violet); +} + +.feature-points { + margin: 18px 0 0; + padding: 0; + list-style: none; +} + +.feature-points li { + position: relative; + padding-left: 18px; + color: var(--pg-muted-strong); + line-height: 1.7; +} + +.feature-points li::before { + content: ''; + position: absolute; + top: 11px; + left: 0; + width: 8px; + height: 8px; + border-radius: 999px; + background: rgba(121, 247, 195, 0.78); +} + +.lab-shell { + display: grid; + gap: 18px; +} + +.lab-shell--stack .lab-grid { + grid-template-columns: 1fr; +} + +.lab-shell--surge .lab-panel { + border-color: rgba(255, 196, 108, 0.28); +} + +.lab-shell--soft .lab-panel { + background: var(--pg-surface-soft); +} + +.lab-shell--calm .lab-panel, +.lab-shell--calm .segment-button, +.lab-shell--calm .action-button, +.lab-shell--calm .jump-card { + transition-duration: 80ms; +} + +.lab-panel { + padding: 24px; + border-radius: var(--pg-radius-lg); +} + +.panel-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 20px; +} + +.panel-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-radius: 999px; + border: 1px solid rgba(121, 201, 255, 0.16); + color: var(--pg-muted-strong); + background: rgba(255, 255, 255, 0.04); + font-size: 0.88rem; +} + +.metric-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-bottom: 18px; +} + +.control-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.control-group { + padding: 16px; + border-radius: var(--pg-radius-md); + border: 1px solid rgba(121, 201, 255, 0.1); + background: rgba(255, 255, 255, 0.025); +} + +.control-title { + margin-bottom: 12px; + color: var(--pg-muted-strong); + font-size: 0.84rem; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.segmented-row, +.stepper-row, +.quick-actions, +.tag-cloud, +.article-meta, +.site-footer__meta { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.segment-button, +.mini-button { + border: 1px solid rgba(121, 201, 255, 0.16); + background: rgba(255, 255, 255, 0.03); + color: var(--pg-muted-strong); + border-radius: 999px; + cursor: pointer; + transition: + transform 180ms ease, + background-color 180ms ease, + border-color 180ms ease; +} + +.segment-button { + padding: 10px 14px; +} + +.segment-button--active { + color: #06131f; + background: linear-gradient(135deg, rgba(22, 182, 246, 0.95), rgba(121, 247, 195, 0.92)); + border-color: transparent; +} + +.mini-button { + width: 36px; + height: 36px; +} + +.segment-button:hover, +.segment-button:focus-visible, +.mini-button:hover, +.mini-button:focus-visible { + transform: translateY(-1px); + border-color: rgba(121, 201, 255, 0.32); +} + +.stepper-row { + align-items: center; +} + +.stepper-row span { + min-width: 24px; + text-align: center; + color: #fff; + font-weight: 700; +} + +.quick-actions { + margin-top: 20px; +} + +.lab-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.resource-card { + padding: 20px; + border-radius: var(--pg-radius-md); + border: 1px solid rgba(121, 201, 255, 0.14); + background: rgba(255, 255, 255, 0.03); +} + +.resource-card--pending { + display: grid; + grid-template-columns: auto 1fr; + gap: 16px; + align-items: center; +} + +.resource-signal { + width: 16px; + height: 80px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(22, 182, 246, 0.1), rgba(22, 182, 246, 0.9)); + box-shadow: 0 0 0 8px rgba(22, 182, 246, 0.09); +} + +.resource-card--error { + border-color: rgba(255, 125, 147, 0.32); + background: rgba(255, 125, 147, 0.08); +} + +.resource-card--empty { + border-color: rgba(255, 196, 108, 0.28); +} + +.resource-card--resolved { + border-color: rgba(121, 247, 195, 0.22); +} + +.resource-state { + margin-bottom: 12px; + color: var(--pg-muted-strong); + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.resource-title { + color: #fff; + font-size: 1.18rem; + font-weight: 700; + line-height: 1.3; +} + +.resource-body { + margin-top: 10px; + color: var(--pg-muted); +} + +.resource-stats { + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 18px; +} + +.event-stream { + margin-top: 20px; +} + +.event-stream__header { + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + color: var(--pg-muted-strong); + font-size: 0.92rem; +} + +.event-list { + display: grid; + gap: 10px; +} + +.event-card { + padding: 14px; + border-radius: var(--pg-radius-sm); + border: 1px solid rgba(121, 201, 255, 0.12); + background: rgba(255, 255, 255, 0.03); +} + +.event-card--success { + border-color: rgba(121, 247, 195, 0.24); +} + +.event-card--warning { + border-color: rgba(255, 196, 108, 0.24); +} + +.event-card__meta { + display: flex; + justify-content: space-between; + gap: 12px; + color: var(--pg-muted-strong); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.event-card__detail { + margin-top: 8px; + color: var(--pg-muted); +} + +.principles-grid, +.article-grid, +.jump-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.principle-card, +.jump-card, +.split-callout { + padding: 22px; + border-radius: var(--pg-radius-lg); +} + +.split-section { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.inspection-list, +.rules-list { + display: grid; + gap: 16px; + margin-top: 10px; +} + +.inspection-row, +.rule-row { + display: grid; + gap: 8px; +} + +.inspection-row { + grid-template-columns: auto 1fr; + align-items: start; +} + +.inspection-row__icon { + margin-top: 4px; + color: var(--pg-cyan); +} + +.rule-row strong, +.inspection-row strong, +.article-card__header span, +.article-status, +.article-category { + color: var(--pg-muted-strong); +} + +.featured-article { + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); + gap: 18px; +} + +.featured-article__copy, +.featured-article__tags { + padding: 8px 4px; +} + +.featured-article__status { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: 16px; + padding: 12px 14px; + border-radius: var(--pg-radius-md); + background: rgba(255, 255, 255, 0.04); + color: #fff; +} + +.tag-pill, +.article-status, +.article-category { + display: inline-flex; + align-items: center; + padding: 7px 10px; + border-radius: 999px; + border: 1px solid rgba(121, 201, 255, 0.16); + background: rgba(255, 255, 255, 0.04); + font-size: 0.82rem; +} + +.article-grid { + gap: 18px; +} + +.article-card { + display: grid; + gap: 16px; + padding: 24px; + border-radius: var(--pg-radius-lg); +} + +.article-card__header { + display: flex; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.article-link { + justify-self: start; +} + +.article-shell { + display: grid; + gap: 22px; +} + +.article-hero { + padding: 30px; +} + +.article-content { + display: grid; + gap: 26px; +} + +.article-section { + padding: 22px 24px; + border-radius: var(--pg-radius-lg); + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(121, 201, 255, 0.1); +} + +.article-section > * + * { + margin-top: 14px; +} + +blockquote { + margin: 0; + padding: 18px 18px 18px 20px; + border-left: 3px solid var(--pg-cyan); + border-radius: 0 16px 16px 0; + background: rgba(22, 182, 246, 0.08); + color: #fff; + font-family: var(--pg-font-display); + font-size: 1.12rem; + line-height: 1.55; +} + +.code-block { + margin: 0; + padding: 18px; + overflow-x: auto; + border-radius: var(--pg-radius-md); + background: rgba(6, 11, 20, 0.86); + border: 1px solid rgba(121, 201, 255, 0.12); +} + +.code-block code { + font-family: 'SFMono-Regular', 'Menlo', 'Consolas', monospace; + color: #b4fff1; + white-space: pre; +} + +.site-footer { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: center; + margin-top: 24px; + padding: 18px 22px; + border-radius: var(--pg-radius-xl); +} + +.site-footer__title { + color: #fff; + font-weight: 700; +} + +.site-footer__copy { + margin-top: 6px; + color: var(--pg-muted); + font-size: 0.92rem; +} + +.site-footer__meta span { + padding: 8px 10px; + border-radius: 999px; + border: 1px solid rgba(121, 201, 255, 0.14); + color: var(--pg-muted-strong); +} + +@media (max-width: 980px) { + .topbar, + .featured-article, + .split-section, + .hero-layout, + .site-footer { + grid-template-columns: 1fr; + } + + .topbar { + justify-items: start; + } + + .topnav { + justify-content: flex-start; + } + + .hero-status-grid, + .metric-strip, + .feature-grid, + .principles-grid, + .article-grid, + .jump-grid, + .lab-grid { + grid-template-columns: 1fr; + } + + .control-grid { + grid-template-columns: 1fr; + } + + .panel-heading, + .event-stream__header { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .app-frame { + width: min(100% - 18px, 100%); + padding-top: 14px; + } + + .topbar, + .section-card, + .page-hero, + .featured-article, + .article-shell, + .hero-copy, + .lab-panel, + .metric-tower { + padding: 20px; + } + + h1 { + font-size: 2.5rem; + } + + h2 { + font-size: 2rem; + } + + .hero-actions, + .quick-actions, + .segmented-row, + .tag-cloud, + .article-meta { + gap: 8px; + } + + .hero-button, + .action-button, + .article-link, + .segment-button { + width: 100%; + } + + .hero-status-grid, + .metric-strip, + .resource-stats, + .environment-grid { + grid-template-columns: 1fr; + } +} diff --git a/packages/playgrounds/src/root.tsx b/packages/playgrounds/src/root.tsx index 83c7f14..7d8b259 100644 --- a/packages/playgrounds/src/root.tsx +++ b/packages/playgrounds/src/root.tsx @@ -25,10 +25,10 @@ export default component$(() => { href={`${import.meta.env.BASE_URL}manifest.json`} /> )} - + - - + + {!isDev && } diff --git a/packages/playgrounds/src/routes/about/index.tsx b/packages/playgrounds/src/routes/about/index.tsx index 608c48e..55cd83e 100644 --- a/packages/playgrounds/src/routes/about/index.tsx +++ b/packages/playgrounds/src/routes/about/index.tsx @@ -1,28 +1,117 @@ import type { DocumentHead } from '@qwik.dev/router'; -import { component$, useSignal } from '@qwik.dev/core'; -import Button from '~/components/Button/Button'; +import { component$ } from '@qwik.dev/core'; +import { Link } from '@qwik.dev/router'; +import { PlaygroundGlyph } from '~/components/playground/icons'; +import { + aboutPrinciples, + visualSystemRules, +} from '~/content/playground-content'; export default component$(() => { - const testValue = useSignal('111'); return ( - <> -
About
-