Updated frontend

This commit is contained in:
2026-05-21 13:38:32 +05:30
parent 130fa7a956
commit 72d954db38
55 changed files with 63068 additions and 3405 deletions
+7 -7
View File
@@ -1,13 +1,13 @@
# ArchStore — Arch Linux Package Store
A modern lightweight package manager client for Arch Linux that combines official `pacman` repositories and the Arch User Repository (AUR) into one clean, elegant Play Store-like interface.
A classic, stable, and practical Linux desktop style package manager frontend for Arch Linux. It combines official `pacman` repositories and the Arch User Repository (AUR) into a dense, functional interface reminiscent of classic utilities like Synaptic Package Manager, Pamac, and older GNOME/XFCE applications.
## Main Features
- **Unified Search**: Search packages across pacman repositories and the AUR simultaneously.
- **Detailed Package Sheets**: View descriptions, maintainers, votes, popularity, and installed statuses.
- **Detailed Package Metadata**: View licenses, sizes, packager, installation dates, dependencies, and installed versions.
- **PKGBUILD Security Scanner**: Analyzes PKGBUILD script manifests for suspicious scripts, remote code execution (curl/wget to sh), command injection, and other threats.
- **System Updates Check**: Checks for updates from both pacman sync databases and the AUR.
- **System Updates Manager**: Dense list split into security bulletins and standard applications, supporting individual selections and upgrade execution.
- **Category Browsing**: Explore applications by genre (Development, System, Networks, Multimedia, Games, etc.).
- **Local SQLite Caching**: Fast indexing and pagination for package queries with a 15-minute Time-to-Live (TTL).
@@ -20,10 +20,10 @@ A modern lightweight package manager client for Arch Linux that combines officia
- Whitelist-based package name and search query sanitization.
- Lightweight SQLite storage cache with auto-expiration.
### Frontend (React + Vite + TailwindCSS v4)
- Responsive dark-mode UI inspired by Arch Linux.
- Fixed sidebar layout collapsing on smaller device widths.
- Shimmer skeleton loaders, micro-animations, and staggered grids.
### Frontend (React + TypeScript + TailwindCSS v4)
- Stable, non-trendy desktop-oriented interface with a fixed sidebar, top toolbar, main work panel, and bottom status bar.
- Pure black dark mode (`#000000`) and clean white light mode with dense spacing, standard table layouts, and classic retro-thin scrollbars.
- Simple rectangular borders with minimal rounding, zero modern gradients, shimmers, or floating card designs.
---
+38
View File
@@ -0,0 +1,38 @@
{
"hash": "de57b94e",
"configHash": "060682df",
"lockfileHash": "f56911e4",
"browserHash": "019f07ab",
"optimized": {
"lucide-react": {
"src": "../../node_modules/lucide-react/dist/esm/lucide-react.mjs",
"file": "lucide-react.js",
"fileHash": "96235559",
"needsInterop": false
},
"react-dom/client": {
"src": "../../node_modules/react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "6692af08",
"needsInterop": true
},
"react": {
"src": "../../node_modules/react/index.js",
"file": "react.js",
"fileHash": "4336e757",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../node_modules/react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "ecb27d4a",
"needsInterop": true
}
},
"chunks": {
"react-DNKNjk6d": {
"file": "react-DNKNjk6d.js",
"isDynamicEntry": false
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+780
View File
@@ -0,0 +1,780 @@
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region node_modules/react/cjs/react.development.js
/**
* @license React
* react.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_development = /* @__PURE__ */ __commonJSMin(((exports, module) => {
(function() {
function defineDeprecationWarning(methodName, info) {
Object.defineProperty(Component.prototype, methodName, { get: function() {
console.warn("%s(...) is deprecated in plain JavaScript React classes. %s", info[0], info[1]);
} });
}
function getIteratorFn(maybeIterable) {
if (null === maybeIterable || "object" !== typeof maybeIterable) return null;
maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"];
return "function" === typeof maybeIterable ? maybeIterable : null;
}
function warnNoop(publicInstance, callerName) {
publicInstance = (publicInstance = publicInstance.constructor) && (publicInstance.displayName || publicInstance.name) || "ReactClass";
var warningKey = publicInstance + "." + callerName;
didWarnStateUpdateForUnmountedComponent[warningKey] || (console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.", callerName, publicInstance), didWarnStateUpdateForUnmountedComponent[warningKey] = !0);
}
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function noop() {}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function cloneAndReplaceKey(oldElement, newKey) {
newKey = ReactElement(oldElement.type, newKey, oldElement.props, oldElement._owner, oldElement._debugStack, oldElement._debugTask);
oldElement._store && (newKey._store.validated = oldElement._store.validated);
return newKey;
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
function escape(key) {
var escaperLookup = {
"=": "=0",
":": "=2"
};
return "$" + key.replace(/[=:]/g, function(match) {
return escaperLookup[match];
});
}
function getElementKey(element, index) {
return "object" === typeof element && null !== element && null != element.key ? (checkKeyStringCoercion(element.key), escape("" + element.key)) : index.toString(36);
}
function resolveThenable(thenable) {
switch (thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
default: switch ("string" === typeof thenable.status ? thenable.then(noop, noop) : (thenable.status = "pending", thenable.then(function(fulfilledValue) {
"pending" === thenable.status && (thenable.status = "fulfilled", thenable.value = fulfilledValue);
}, function(error) {
"pending" === thenable.status && (thenable.status = "rejected", thenable.reason = error);
})), thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
}
}
throw thenable;
}
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
var type = typeof children;
if ("undefined" === type || "boolean" === type) children = null;
var invokeCallback = !1;
if (null === children) invokeCallback = !0;
else switch (type) {
case "bigint":
case "string":
case "number":
invokeCallback = !0;
break;
case "object": switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = !0;
break;
case REACT_LAZY_TYPE: return invokeCallback = children._init, mapIntoArray(invokeCallback(children._payload), array, escapedPrefix, nameSoFar, callback);
}
}
if (invokeCallback) {
invokeCallback = children;
callback = callback(invokeCallback);
var childKey = "" === nameSoFar ? "." + getElementKey(invokeCallback, 0) : nameSoFar;
isArrayImpl(callback) ? (escapedPrefix = "", null != childKey && (escapedPrefix = childKey.replace(userProvidedKeyEscapeRegex, "$&/") + "/"), mapIntoArray(callback, array, escapedPrefix, "", function(c) {
return c;
})) : null != callback && (isValidElement(callback) && (null != callback.key && (invokeCallback && invokeCallback.key === callback.key || checkKeyStringCoercion(callback.key)), escapedPrefix = cloneAndReplaceKey(callback, escapedPrefix + (null == callback.key || invokeCallback && invokeCallback.key === callback.key ? "" : ("" + callback.key).replace(userProvidedKeyEscapeRegex, "$&/") + "/") + childKey), "" !== nameSoFar && null != invokeCallback && isValidElement(invokeCallback) && null == invokeCallback.key && invokeCallback._store && !invokeCallback._store.validated && (escapedPrefix._store.validated = 2), callback = escapedPrefix), array.push(callback));
return 1;
}
invokeCallback = 0;
childKey = "" === nameSoFar ? "." : nameSoFar + ":";
if (isArrayImpl(children)) for (var i = 0; i < children.length; i++) nameSoFar = children[i], type = childKey + getElementKey(nameSoFar, i), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if (i = getIteratorFn(children), "function" === typeof i) for (i === children.entries && (didWarnAboutMaps || console.warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."), didWarnAboutMaps = !0), children = i.call(children), i = 0; !(nameSoFar = children.next()).done;) nameSoFar = nameSoFar.value, type = childKey + getElementKey(nameSoFar, i++), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if ("object" === type) {
if ("function" === typeof children.then) return mapIntoArray(resolveThenable(children), array, escapedPrefix, nameSoFar, callback);
array = String(children);
throw Error("Objects are not valid as a React child (found: " + ("[object Object]" === array ? "object with keys {" + Object.keys(children).join(", ") + "}" : array) + "). If you meant to render a collection of children, use an array instead.");
}
return invokeCallback;
}
function mapChildren(children, func, context) {
if (null == children) return children;
var result = [], count = 0;
mapIntoArray(children, result, "", "", function(child) {
return func.call(context, child, count++);
});
return result;
}
function lazyInitializer(payload) {
if (-1 === payload._status) {
var ioInfo = payload._ioInfo;
null != ioInfo && (ioInfo.start = ioInfo.end = performance.now());
ioInfo = payload._result;
var thenable = ioInfo();
thenable.then(function(moduleObject) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 1;
payload._result = moduleObject;
var _ioInfo = payload._ioInfo;
null != _ioInfo && (_ioInfo.end = performance.now());
void 0 === thenable.status && (thenable.status = "fulfilled", thenable.value = moduleObject);
}
}, function(error) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 2;
payload._result = error;
var _ioInfo2 = payload._ioInfo;
null != _ioInfo2 && (_ioInfo2.end = performance.now());
void 0 === thenable.status && (thenable.status = "rejected", thenable.reason = error);
}
});
ioInfo = payload._ioInfo;
if (null != ioInfo) {
ioInfo.value = thenable;
var displayName = thenable.displayName;
"string" === typeof displayName && (ioInfo.name = displayName);
}
-1 === payload._status && (payload._status = 0, payload._result = thenable);
}
if (1 === payload._status) return ioInfo = payload._result, void 0 === ioInfo && console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))\n\nDid you accidentally put curly braces around the import?", ioInfo), "default" in ioInfo || console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))", ioInfo), ioInfo.default;
throw payload._result;
}
function resolveDispatcher() {
var dispatcher = ReactSharedInternals.H;
null === dispatcher && console.error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.");
return dispatcher;
}
function releaseAsyncTransition() {
ReactSharedInternals.asyncTransitions--;
}
function enqueueTask(task) {
if (null === enqueueTaskImpl) try {
var requireString = ("require" + Math.random()).slice(0, 7);
enqueueTaskImpl = (module && module[requireString]).call(module, "timers").setImmediate;
} catch (_err) {
enqueueTaskImpl = function(callback) {
!1 === didWarnAboutMessageChannel && (didWarnAboutMessageChannel = !0, "undefined" === typeof MessageChannel && console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));
var channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(void 0);
};
}
return enqueueTaskImpl(task);
}
function aggregateErrors(errors) {
return 1 < errors.length && "function" === typeof AggregateError ? new AggregateError(errors) : errors[0];
}
function popActScope(prevActQueue, prevActScopeDepth) {
prevActScopeDepth !== actScopeDepth - 1 && console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
actScopeDepth = prevActScopeDepth;
}
function recursivelyFlushAsyncActWork(returnValue, resolve, reject) {
var queue = ReactSharedInternals.actQueue;
if (null !== queue) if (0 !== queue.length) try {
flushActQueue(queue);
enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
return;
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
else ReactSharedInternals.actQueue = null;
0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve(returnValue);
}
function flushActQueue(queue) {
if (!isFlushing) {
isFlushing = !0;
var i = 0;
try {
for (; i < queue.length; i++) {
var callback = queue[i];
do {
ReactSharedInternals.didUsePromise = !1;
var continuation = callback(!1);
if (null !== continuation) {
if (ReactSharedInternals.didUsePromise) {
queue[i] = callback;
queue.splice(0, i);
return;
}
callback = continuation;
} else break;
} while (1);
}
queue.length = 0;
} catch (error) {
queue.splice(0, i + 1), ReactSharedInternals.thrownErrors.push(error);
} finally {
isFlushing = !1;
}
}
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator, didWarnStateUpdateForUnmountedComponent = {}, ReactNoopUpdateQueue = {
isMounted: function() {
return !1;
},
enqueueForceUpdate: function(publicInstance) {
warnNoop(publicInstance, "forceUpdate");
},
enqueueReplaceState: function(publicInstance) {
warnNoop(publicInstance, "replaceState");
},
enqueueSetState: function(publicInstance) {
warnNoop(publicInstance, "setState");
}
}, assign = Object.assign, emptyObject = {};
Object.freeze(emptyObject);
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) throw Error("takes an object of state variables to update or a function which returns an object of state variables.");
this.updater.enqueueSetState(this, partialState, callback, "setState");
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};
var deprecatedAPIs = {
isMounted: ["isMounted", "Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."],
replaceState: ["replaceState", "Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."]
};
for (fnName in deprecatedAPIs) deprecatedAPIs.hasOwnProperty(fnName) && defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
ComponentDummy.prototype = Component.prototype;
deprecatedAPIs = PureComponent.prototype = new ComponentDummy();
deprecatedAPIs.constructor = PureComponent;
assign(deprecatedAPIs, Component.prototype);
deprecatedAPIs.isPureReactComponent = !0;
var isArrayImpl = Array.isArray, REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = {
H: null,
A: null,
T: null,
S: null,
actQueue: null,
asyncTransitions: 0,
isBatchingLegacy: !1,
didScheduleLegacyUpdate: !1,
didUsePromise: !1,
thrownErrors: [],
getCurrentStack: null,
recentlyCreatedOwnerStacks: 0
}, hasOwnProperty = Object.prototype.hasOwnProperty, createTask = console.createTask ? console.createTask : function() {
return null;
};
deprecatedAPIs = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown, didWarnAboutOldJSXRuntime;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = deprecatedAPIs.react_stack_bottom_frame.bind(deprecatedAPIs, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutMaps = !1, userProvidedKeyEscapeRegex = /\/+/g, reportGlobalError = "function" === typeof reportError ? reportError : function(error) {
if ("object" === typeof window && "function" === typeof window.ErrorEvent) {
var event = new window.ErrorEvent("error", {
bubbles: !0,
cancelable: !0,
message: "object" === typeof error && null !== error && "string" === typeof error.message ? String(error.message) : String(error),
error
});
if (!window.dispatchEvent(event)) return;
} else if ("object" === typeof process && "function" === typeof process.emit) {
process.emit("uncaughtException", error);
return;
}
console.error(error);
}, didWarnAboutMessageChannel = !1, enqueueTaskImpl = null, actScopeDepth = 0, didWarnNoAwaitAct = !1, isFlushing = !1, queueSeveralMicrotasks = "function" === typeof queueMicrotask ? function(callback) {
queueMicrotask(function() {
return queueMicrotask(callback);
});
} : enqueueTask;
deprecatedAPIs = Object.freeze({
__proto__: null,
c: function(size) {
return resolveDispatcher().useMemoCache(size);
}
});
var fnName = {
map: mapChildren,
forEach: function(children, forEachFunc, forEachContext) {
mapChildren(children, function() {
forEachFunc.apply(this, arguments);
}, forEachContext);
},
count: function(children) {
var n = 0;
mapChildren(children, function() {
n++;
});
return n;
},
toArray: function(children) {
return mapChildren(children, function(child) {
return child;
}) || [];
},
only: function(children) {
if (!isValidElement(children)) throw Error("React.Children.only expected to receive a single React element child.");
return children;
}
};
exports.Activity = REACT_ACTIVITY_TYPE;
exports.Children = fnName;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
exports.PureComponent = PureComponent;
exports.StrictMode = REACT_STRICT_MODE_TYPE;
exports.Suspense = REACT_SUSPENSE_TYPE;
exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals;
exports.__COMPILER_RUNTIME = deprecatedAPIs;
exports.act = function(callback) {
var prevActQueue = ReactSharedInternals.actQueue, prevActScopeDepth = actScopeDepth;
actScopeDepth++;
var queue = ReactSharedInternals.actQueue = null !== prevActQueue ? prevActQueue : [], didAwaitActCall = !1;
try {
var result = callback();
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
if (0 < ReactSharedInternals.thrownErrors.length) throw popActScope(prevActQueue, prevActScopeDepth), callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
if (null !== result && "object" === typeof result && "function" === typeof result.then) {
var thenable = result;
queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"));
});
return { then: function(resolve, reject) {
didAwaitActCall = !0;
thenable.then(function(returnValue) {
popActScope(prevActQueue, prevActScopeDepth);
if (0 === prevActScopeDepth) {
try {
flushActQueue(queue), enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
} catch (error$0) {
ReactSharedInternals.thrownErrors.push(error$0);
}
if (0 < ReactSharedInternals.thrownErrors.length) {
var _thrownError = aggregateErrors(ReactSharedInternals.thrownErrors);
ReactSharedInternals.thrownErrors.length = 0;
reject(_thrownError);
}
} else resolve(returnValue);
}, function(error) {
popActScope(prevActQueue, prevActScopeDepth);
0 < ReactSharedInternals.thrownErrors.length ? (error = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(error)) : reject(error);
});
} };
}
var returnValue$jscomp$0 = result;
popActScope(prevActQueue, prevActScopeDepth);
0 === prevActScopeDepth && (flushActQueue(queue), 0 !== queue.length && queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("A component suspended inside an `act` scope, but the `act` call was not awaited. When testing React components that depend on asynchronous data, you must await the result:\n\nawait act(() => ...)"));
}), ReactSharedInternals.actQueue = null);
if (0 < ReactSharedInternals.thrownErrors.length) throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
return { then: function(resolve, reject) {
didAwaitActCall = !0;
0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve, reject);
})) : resolve(returnValue$jscomp$0);
} };
};
exports.cache = function(fn) {
return function() {
return fn.apply(null, arguments);
};
};
exports.cacheSignal = function() {
return null;
};
exports.captureOwnerStack = function() {
var getCurrentStack = ReactSharedInternals.getCurrentStack;
return null === getCurrentStack ? null : getCurrentStack();
};
exports.cloneElement = function(element, config, children) {
if (null === element || void 0 === element) throw Error("The argument must be a React element, but you passed " + element + ".");
var props = assign({}, element.props), key = element.key, owner = element._owner;
if (null != config) {
var JSCompiler_inline_result;
a: {
if (hasOwnProperty.call(config, "ref") && (JSCompiler_inline_result = Object.getOwnPropertyDescriptor(config, "ref").get) && JSCompiler_inline_result.isReactWarning) {
JSCompiler_inline_result = !1;
break a;
}
JSCompiler_inline_result = void 0 !== config.ref;
}
JSCompiler_inline_result && (owner = getOwner());
hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key);
for (propName in config) !hasOwnProperty.call(config, propName) || "key" === propName || "__self" === propName || "__source" === propName || "ref" === propName && void 0 === config.ref || (props[propName] = config[propName]);
}
var propName = arguments.length - 2;
if (1 === propName) props.children = children;
else if (1 < propName) {
JSCompiler_inline_result = Array(propName);
for (var i = 0; i < propName; i++) JSCompiler_inline_result[i] = arguments[i + 2];
props.children = JSCompiler_inline_result;
}
props = ReactElement(element.type, key, props, owner, element._debugStack, element._debugTask);
for (key = 2; key < arguments.length; key++) validateChildKeys(arguments[key]);
return props;
};
exports.createContext = function(defaultValue) {
defaultValue = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null
};
defaultValue.Provider = defaultValue;
defaultValue.Consumer = {
$$typeof: REACT_CONSUMER_TYPE,
_context: defaultValue
};
defaultValue._currentRenderer = null;
defaultValue._currentRenderer2 = null;
return defaultValue;
};
exports.createElement = function(type, config, children) {
for (var i = 2; i < arguments.length; i++) validateChildKeys(arguments[i]);
i = {};
var key = null;
if (null != config) for (propName in didWarnAboutOldJSXRuntime || !("__self" in config) || "key" in config || (didWarnAboutOldJSXRuntime = !0, console.warn("Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform")), hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key), config) hasOwnProperty.call(config, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (i[propName] = config[propName]);
var childrenLength = arguments.length - 2;
if (1 === childrenLength) i.children = children;
else if (1 < childrenLength) {
for (var childArray = Array(childrenLength), _i = 0; _i < childrenLength; _i++) childArray[_i] = arguments[_i + 2];
Object.freeze && Object.freeze(childArray);
i.children = childArray;
}
if (type && type.defaultProps) for (propName in childrenLength = type.defaultProps, childrenLength) void 0 === i[propName] && (i[propName] = childrenLength[propName]);
key && defineKeyPropWarningGetter(i, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
var propName = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
return ReactElement(type, key, i, getOwner(), propName ? Error("react-stack-top-frame") : unknownOwnerDebugStack, propName ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
exports.createRef = function() {
var refObject = { current: null };
Object.seal(refObject);
return refObject;
};
exports.forwardRef = function(render) {
null != render && render.$$typeof === REACT_MEMO_TYPE ? console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...)).") : "function" !== typeof render ? console.error("forwardRef requires a render function but was given %s.", null === render ? "null" : typeof render) : 0 !== render.length && 2 !== render.length && console.error("forwardRef render functions accept exactly two parameters: props and ref. %s", 1 === render.length ? "Did you forget to use the ref parameter?" : "Any additional parameter will be undefined.");
null != render && null != render.defaultProps && console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");
var elementType = {
$$typeof: REACT_FORWARD_REF_TYPE,
render
}, ownName;
Object.defineProperty(elementType, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
render.name || render.displayName || (Object.defineProperty(render, "name", { value: name }), render.displayName = name);
}
});
return elementType;
};
exports.isValidElement = isValidElement;
exports.lazy = function(ctor) {
ctor = {
_status: -1,
_result: ctor
};
var lazyType = {
$$typeof: REACT_LAZY_TYPE,
_payload: ctor,
_init: lazyInitializer
}, ioInfo = {
name: "lazy",
start: -1,
end: -1,
value: null,
owner: null,
debugStack: Error("react-stack-top-frame"),
debugTask: console.createTask ? console.createTask("lazy()") : null
};
ctor._ioInfo = ioInfo;
lazyType._debugInfo = [{ awaited: ioInfo }];
return lazyType;
};
exports.memo = function(type, compare) {
type ?? console.error("memo: The first argument must be a component. Instead received: %s", null === type ? "null" : typeof type);
compare = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: void 0 === compare ? null : compare
};
var ownName;
Object.defineProperty(compare, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
type.name || type.displayName || (Object.defineProperty(type, "name", { value: name }), type.displayName = name);
}
});
return compare;
};
exports.startTransition = function(scope) {
var prevTransition = ReactSharedInternals.T, currentTransition = {};
currentTransition._updatedFibers = /* @__PURE__ */ new Set();
ReactSharedInternals.T = currentTransition;
try {
var returnValue = scope(), onStartTransitionFinish = ReactSharedInternals.S;
null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue);
"object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && (ReactSharedInternals.asyncTransitions++, returnValue.then(releaseAsyncTransition, releaseAsyncTransition), returnValue.then(noop, reportGlobalError));
} catch (error) {
reportGlobalError(error);
} finally {
null === prevTransition && currentTransition._updatedFibers && (scope = currentTransition._updatedFibers.size, currentTransition._updatedFibers.clear(), 10 < scope && console.warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table.")), null !== prevTransition && null !== currentTransition.types && (null !== prevTransition.types && prevTransition.types !== currentTransition.types && console.error("We expected inner Transitions to have transferred the outer types set and that you cannot add to the outer Transition while inside the inner.This is a bug in React."), prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition;
}
};
exports.unstable_useCacheRefresh = function() {
return resolveDispatcher().useCacheRefresh();
};
exports.use = function(usable) {
return resolveDispatcher().use(usable);
};
exports.useActionState = function(action, initialState, permalink) {
return resolveDispatcher().useActionState(action, initialState, permalink);
};
exports.useCallback = function(callback, deps) {
return resolveDispatcher().useCallback(callback, deps);
};
exports.useContext = function(Context) {
var dispatcher = resolveDispatcher();
Context.$$typeof === REACT_CONSUMER_TYPE && console.error("Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you mean to call useContext(Context) instead?");
return dispatcher.useContext(Context);
};
exports.useDebugValue = function(value, formatterFn) {
return resolveDispatcher().useDebugValue(value, formatterFn);
};
exports.useDeferredValue = function(value, initialValue) {
return resolveDispatcher().useDeferredValue(value, initialValue);
};
exports.useEffect = function(create, deps) {
create ?? console.warn("React Hook useEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useEffect(create, deps);
};
exports.useEffectEvent = function(callback) {
return resolveDispatcher().useEffectEvent(callback);
};
exports.useId = function() {
return resolveDispatcher().useId();
};
exports.useImperativeHandle = function(ref, create, deps) {
return resolveDispatcher().useImperativeHandle(ref, create, deps);
};
exports.useInsertionEffect = function(create, deps) {
create ?? console.warn("React Hook useInsertionEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useInsertionEffect(create, deps);
};
exports.useLayoutEffect = function(create, deps) {
create ?? console.warn("React Hook useLayoutEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useLayoutEffect(create, deps);
};
exports.useMemo = function(create, deps) {
return resolveDispatcher().useMemo(create, deps);
};
exports.useOptimistic = function(passthrough, reducer) {
return resolveDispatcher().useOptimistic(passthrough, reducer);
};
exports.useReducer = function(reducer, initialArg, init) {
return resolveDispatcher().useReducer(reducer, initialArg, init);
};
exports.useRef = function(initialValue) {
return resolveDispatcher().useRef(initialValue);
};
exports.useState = function(initialState) {
return resolveDispatcher().useState(initialState);
};
exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
return resolveDispatcher().useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
};
exports.useTransition = function() {
return resolveDispatcher().useTransition();
};
exports.version = "19.2.6";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/react/index.js
var require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_development();
}));
//#endregion
export { __commonJSMin as n, __exportAll as r, require_react as t };
//# sourceMappingURL=react-DNKNjk6d.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
import { t as require_react } from "./react-DNKNjk6d.js";
export default require_react();
@@ -0,0 +1,204 @@
import { n as __commonJSMin, t as require_react } from "./react-DNKNjk6d.js";
//#region node_modules/react/cjs/react-jsx-dev-runtime.development.js
/**
* @license React
* react-jsx-dev-runtime.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_jsx_dev_runtime_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStack, debugTask) {
var children = config.children;
if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) {
for (isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++) validateChildKeys(children[isStaticChildren]);
Object.freeze && Object.freeze(children);
} else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
else validateChildKeys(children);
if (hasOwnProperty.call(config, "key")) {
children = getComponentNameFromType(type);
var keys = Object.keys(config).filter(function(k) {
return "key" !== k;
});
isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
didWarnAboutKeySpread[children + isStaticChildren] || (keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}", console.error("A props object containing a \"key\" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />", isStaticChildren, children, keys, children), didWarnAboutKeySpread[children + isStaticChildren] = !0);
}
children = null;
void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), children = "" + maybeKey);
hasValidKey(config) && (checkKeyStringCoercion(config.key), children = "" + config.key);
if ("key" in config) {
maybeKey = {};
for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
} else maybeKey = config;
children && defineKeyPropWarningGetter(maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
return ReactElement(type, children, maybeKey, getOwner(), debugStack, debugTask);
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
var React = require_react(), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
return null;
};
React = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutKeySpread = {};
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsxDEV = function(type, config, maybeKey, isStaticChildren) {
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
return jsxDEVImpl(type, config, maybeKey, isStaticChildren, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
})();
}));
//#endregion
//#region node_modules/react/jsx-dev-runtime.js
var require_jsx_dev_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_jsx_dev_runtime_development();
}));
//#endregion
export default require_jsx_dev_runtime();
//# sourceMappingURL=react_jsx-dev-runtime.js.map
File diff suppressed because one or more lines are too long
+59 -2
View File
@@ -1,4 +1,4 @@
# React + Vite
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
@@ -13,4 +13,61 @@ The React Compiler is not enabled on this template because of its impact on dev
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+3 -2
View File
@@ -2,20 +2,21 @@ import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
])
+4 -8
View File
@@ -1,18 +1,14 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ArchStore — A modern lightweight package store for Arch Linux combining pacman and AUR." />
<meta name="theme-color" content="#0d1117" />
<title>ArchStore — Arch Linux Package Store</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<meta name="description" content="ArchStore — A package manager for Arch Linux combining pacman and AUR." />
<title>ArchStore</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+398 -194
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -5,19 +5,21 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.15.1"
"react-router-dom": "^7.15.1",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
@@ -25,7 +27,8 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}
+3 -10
View File
@@ -1,11 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1793d1"/>
<stop offset="100%" stop-color="#0f6ea8"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="#0d1117"/>
<path d="M32 8L12 52h10l10-22 10 22h10L32 8z" fill="url(#g)"/>
<circle cx="32" cy="38" r="3" fill="#0d1117"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#1793D1"/>
<text x="16" y="23" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="20" fill="white">A</text>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 246 B

-184
View File
@@ -1,184 +0,0 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
-28
View File
@@ -1,28 +0,0 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Home from './pages/Home';
import Search from './pages/Search';
import Installed from './pages/Installed';
import Updates from './pages/Updates';
import Categories from './pages/Categories';
import Settings from './pages/Settings';
import PackageView from './pages/PackageView';
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="search" element={<Search />} />
<Route path="installed" element={<Installed />} />
<Route path="updates" element={<Updates />} />
<Route path="categories" element={<Categories />} />
<Route path="categories/:categoryName" element={<Categories />} />
<Route path="settings" element={<Settings />} />
<Route path="package/:packageName" element={<PackageView />} />
</Route>
</Routes>
</Router>
);
}
+109
View File
@@ -0,0 +1,109 @@
import { useState, useEffect } from 'react';
import { useTheme } from './lib/theme';
import { api } from './lib/api';
import Sidebar from './components/Sidebar';
import Toolbar from './components/Toolbar';
import StatusBar from './components/StatusBar';
import Dashboard from './pages/Dashboard';
import SearchPage from './pages/Search';
import Installed from './pages/Installed';
import Updates from './pages/Updates';
import Categories from './pages/Categories';
import SettingsPage from './pages/Settings';
import PackageDetails from './pages/PackageDetails';
import type { Page } from './types';
export default function App() {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [activePage, setActivePage] = useState<Page>('dashboard');
const [selectedPackage, setSelectedPackage] = useState<string | null>(null);
const [previousPage, setPreviousPage] = useState<Page>('dashboard');
const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking');
const [installedCount, setInstalledCount] = useState(0);
const [updatesCount, setUpdatesCount] = useState(0);
useEffect(() => {
// Check backend health
api.health()
.then(() => setBackendStatus('connected'))
.catch(() => setBackendStatus('disconnected'));
// Get installed count
api.listInstalled()
.then(data => setInstalledCount(data.count))
.catch(() => {});
// Get updates count
api.checkUpdates()
.then(data => setUpdatesCount(data.count))
.catch(() => {});
}, []);
const navigate = (page: Page) => {
setSelectedPackage(null);
setActivePage(page);
};
const selectPackage = (name: string) => {
setPreviousPage(activePage);
setSelectedPackage(name);
setActivePage('package-details');
};
const goBack = () => {
setSelectedPackage(null);
setActivePage(previousPage);
};
const handleSync = () => {
setBackendStatus('checking');
api.health()
.then(() => setBackendStatus('connected'))
.catch(() => setBackendStatus('disconnected'));
api.checkUpdates()
.then(data => setUpdatesCount(data.count))
.catch(() => {});
};
const renderPage = () => {
if (activePage === 'package-details' && selectedPackage) {
return <PackageDetails packageName={selectedPackage} onBack={goBack} />;
}
switch (activePage) {
case 'dashboard':
return <Dashboard onNavigate={navigate} onSelectPackage={selectPackage} />;
case 'search':
return <SearchPage onSelectPackage={selectPackage} />;
case 'installed':
return <Installed onSelectPackage={selectPackage} />;
case 'updates':
return <Updates onSelectPackage={selectPackage} onUpdatesLoaded={setUpdatesCount} />;
case 'categories':
return <Categories onSelectPackage={selectPackage} />;
case 'settings':
return <SettingsPage />;
default:
return <Dashboard onNavigate={navigate} onSelectPackage={selectPackage} />;
}
};
return (
<div className={`h-screen flex flex-col ${isDark ? 'bg-dark-bg text-dark-text' : 'bg-light-bg text-light-text'}`}>
<div className="flex flex-1 min-h-0">
<Sidebar activePage={activePage} onNavigate={navigate} />
<div className="flex-1 flex flex-col min-w-0">
<Toolbar onNavigate={navigate} onSync={handleSync} updatesCount={updatesCount} />
<main className="flex-1 min-h-0 overflow-hidden">
{renderPage()}
</main>
</div>
</div>
<StatusBar
backendStatus={backendStatus}
installedCount={installedCount}
updatesCount={updatesCount}
/>
</div>
);
}
-72
View File
@@ -1,72 +0,0 @@
/**
* ArchStore API Client
* Handles all communication with the FastAPI backend.
*/
const BASE_URL = '/api';
async function request(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const config = {
headers: { 'Content-Type': 'application/json' },
...options,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || `Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.message === 'Failed to fetch') {
throw new Error('Cannot connect to ArchStore backend. Is the server running?');
}
throw error;
}
}
export const api = {
// Package operations
searchPackages: (query, source = 'all') =>
request(`/packages/search?q=${encodeURIComponent(query)}&source=${source}`),
getPackageInfo: (name) =>
request(`/packages/${encodeURIComponent(name)}`),
scanPackage: (name) =>
request(`/packages/${encodeURIComponent(name)}/scan`),
installPackage: (name) =>
request(`/packages/${encodeURIComponent(name)}/install`, { method: 'POST' }),
removePackage: (name) =>
request(`/packages/${encodeURIComponent(name)}/remove`, { method: 'POST' }),
listInstalled: () =>
request('/packages/installed'),
// Updates
checkUpdates: () =>
request('/updates/check'),
applyUpdates: () =>
request('/updates/apply', { method: 'POST' }),
// Categories
listCategories: () =>
request('/categories'),
getCategoryPackages: (name) =>
request(`/categories/${encodeURIComponent(name)}`),
// System
clearCache: () =>
request('/cache/clear', { method: 'POST' }),
healthCheck: () =>
request('/health'),
};
export default api;
@@ -1,9 +0,0 @@
export default function LoadingSpinner({ size = 'md', text = '' }) {
const px = { sm: 16, md: 20, lg: 28 }[size] || 20;
return (
<div className="flex flex-col items-center justify-center gap-3 py-10">
<div className="spinner" style={{ width: px, height: px }}></div>
{text && <p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{text}</p>}
</div>
);
}
-70
View File
@@ -1,70 +0,0 @@
import { useNavigate } from 'react-router-dom';
import { CheckCircle, AlertTriangle, Star, Download } from 'lucide-react';
export default function PackageCard({ pkg }) {
const navigate = useNavigate();
const sourceBadge = pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman';
const sourceLabel = pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman');
return (
<div
className="card card-interactive package-card p-4 flex flex-col gap-3 cursor-pointer"
onClick={() => navigate(`/package/${pkg.name}`)}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && navigate(`/package/${pkg.name}`)}
>
{/* Header */}
<div className="flex items-start justify-between gap-2 min-w-0">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-[15px] truncate" style={{ color: 'var(--text-primary)' }}>
{pkg.name}
</span>
{pkg.installed && <CheckCircle size={13} style={{ color: 'var(--green)' }} />}
{pkg.out_of_date && <AlertTriangle size={13} style={{ color: 'var(--amber)' }} />}
</div>
<span className="text-[12px] font-mono" style={{ color: 'var(--text-tertiary)' }}>
{pkg.version || '—'}
</span>
</div>
<span className={`badge ${sourceBadge} shrink-0`}>{sourceLabel}</span>
</div>
{/* Description */}
<p className="text-[13px] leading-relaxed"
style={{
color: pkg.description ? 'var(--text-secondary)' : 'var(--text-tertiary)',
fontStyle: pkg.description ? 'normal' : 'italic',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
minHeight: '2.4em'
}}>
{pkg.description || 'No description available'}
</p>
{/* Footer */}
<div className="flex items-center justify-between pt-3 mt-auto"
style={{ borderTop: '1px solid var(--border-primary)' }}>
<div className="flex items-center gap-3 text-[12px]" style={{ color: 'var(--text-tertiary)' }}>
{pkg.votes !== undefined && pkg.votes > 0 && (
<span className="flex items-center gap-0.5">
<Star size={12} style={{ color: 'var(--amber)', fill: 'var(--amber)' }} /> {pkg.votes}
</span>
)}
{pkg.popularity > 0 && <span>{pkg.popularity.toFixed(1)}</span>}
</div>
{pkg.installed ? (
<span className="badge badge-installed text-[10px]">Installed</span>
) : (
<span className="flex items-center gap-1.5 text-[12px] font-semibold"
style={{ color: 'var(--accent)' }}>
<Download size={12} /> Install
</span>
)}
</div>
</div>
);
}
-43
View File
@@ -1,43 +0,0 @@
import PackageCard from './PackageCard';
import { PackageOpen } from 'lucide-react';
export default function PackageGrid({ packages, loading }) {
if (loading) {
return (
<div className="pkg-grid">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card p-3 flex flex-col gap-2">
<div className="flex justify-between">
<div className="shimmer h-4 w-28"></div>
<div className="shimmer h-4 w-12 rounded-full"></div>
</div>
<div className="shimmer h-3 w-16"></div>
<div className="shimmer h-3 w-full"></div>
<div className="shimmer h-3 w-3/4"></div>
</div>
))}
</div>
);
}
if (!packages || packages.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12" style={{ color: 'var(--text-tertiary)' }}>
<div className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)' }}>
<PackageOpen size={20} />
</div>
<p className="text-sm font-medium mb-0.5" style={{ color: 'var(--text-secondary)' }}>No packages found</p>
<p className="text-xs">Try adjusting your search or filters</p>
</div>
);
}
return (
<div className="pkg-grid stagger">
{packages.map((pkg) => (
<PackageCard key={`${pkg.source}-${pkg.name}`} pkg={pkg} />
))}
</div>
);
}
-31
View File
@@ -1,31 +0,0 @@
import { useState, useCallback } from 'react';
import { Search } from 'lucide-react';
export default function SearchBar({ onSearch, initialQuery = '' }) {
const [query, setQuery] = useState(initialQuery);
const handleSubmit = useCallback((e) => {
e.preventDefault();
if (query.trim()) onSearch(query);
}, [query, onSearch]);
return (
<form onSubmit={handleSubmit} className="searchbar">
<Search
size={18}
className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none"
style={{ color: 'var(--text-tertiary)' }}
/>
<input
id="search-input"
type="text"
className="input input-search pl-14 pr-4 rounded-xl"
placeholder="Search packages, AUR helpers, or categories"
value={query}
onChange={(e) => setQuery(e.target.value)}
autoComplete="off"
spellCheck="false"
/>
</form>
);
}
-128
View File
@@ -1,128 +0,0 @@
import { NavLink } from 'react-router-dom';
import { Home, Search, Package, RefreshCw, Grid3X3, Settings, X, Shield, Terminal, Server } from 'lucide-react';
import { useState, useEffect } from 'react';
import api from '../api/client';
const navItems = [
{ path: '/', icon: Home, label: 'Dashboard', section: 'Overview' },
{ path: '/search', icon: Search, label: 'Search Packages', section: 'Manage' },
{ path: '/installed', icon: Package, label: 'Installed', section: 'Manage' },
{ path: '/updates', icon: RefreshCw, label: 'Updates', section: 'Manage' },
{ path: '/categories', icon: Grid3X3, label: 'Browse Categories', section: 'Explore' },
{ path: '/settings', icon: Settings, label: 'Preferences', section: 'System' },
];
export default function Sidebar({ isOpen, onClose }) {
const [updateCount, setUpdateCount] = useState(0);
const [dbSize, setDbSize] = useState('0.0 MB');
const [status, setStatus] = useState('Checking...');
useEffect(() => {
// Gather system health and update indicators
api.checkUpdates()
.then(data => setUpdateCount(data.count || 0))
.catch(() => {});
api.healthCheck()
.then(health => {
if (health.status === 'healthy') {
setStatus('Healthy');
setDbSize(health.database_size || '1.2 MB');
} else {
setStatus('Degraded');
}
})
.catch(() => setStatus('Offline'));
}, []);
// Group nav items by section
const sections = navItems.reduce((acc, item) => {
if (!acc[item.section]) acc[item.section] = [];
acc[item.section].push(item);
return acc;
}, {});
return (
<aside className={`sidebar ${isOpen ? 'open' : ''}`}>
{/* Brand & Logo Header */}
<div className="flex items-center justify-between mb-8 px-2">
<NavLink to="/" className="flex items-center gap-3 no-underline" onClick={onClose}>
<div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)' }}>
<Terminal size={18} style={{ color: 'var(--accent)' }} />
</div>
<div>
<span className="text-base font-semibold block leading-tight" style={{ color: 'var(--text-primary)' }}>
ArchStore
</span>
<span className="text-[10px] leading-tight font-semibold uppercase tracking-wide block" style={{ color: 'var(--text-tertiary)' }}>
Package Manager
</span>
</div>
</NavLink>
<button className="lg:hidden btn-ghost !p-1.5 rounded-lg" onClick={onClose} aria-label="Close">
<X size={16} />
</button>
</div>
{/* Grouped Sidebar Navigation */}
<nav className="flex flex-col gap-5 flex-1 overflow-y-auto pr-1">
{Object.entries(sections).map(([sectionName, items]) => (
<div key={sectionName} className="flex flex-col gap-1">
<span className="text-[10px] font-semibold uppercase tracking-wide px-3 mb-1"
style={{ color: 'var(--text-tertiary)' }}>
{sectionName}
</span>
{items.map(({ path, icon: Icon, label }) => (
<NavLink
key={path}
to={path}
end={path === '/'}
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
onClick={onClose}
>
<Icon size={18} strokeWidth={1.8} />
<span className="font-medium">{label}</span>
{label === 'Updates' && updateCount > 0 && (
<span className="ml-auto text-[10px] font-semibold px-2 py-0.5 rounded-full"
style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border-primary)' }}>
{updateCount}
</span>
)}
</NavLink>
))}
</div>
))}
</nav>
{/* bottom system status dock */}
<div className="pt-4 mt-auto border-t border-[var(--border-primary)] flex flex-col gap-3">
<div className="flex items-center justify-between px-2 text-[11px]">
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-tertiary)' }}>
<Server size={11} /> API Service
</span>
<span className="font-semibold flex items-center gap-1"
style={{ color: status === 'Healthy' ? 'var(--green)' : 'var(--red)' }}>
<span className="w-1.5 h-1.5 rounded-full" style={{ background: status === 'Healthy' ? 'var(--green)' : 'var(--red)' }}></span>
{status}
</span>
</div>
<div className="flex items-center justify-between px-2 text-[11px]">
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-tertiary)' }}>
<Shield size={11} /> Package DB
</span>
<span className="font-mono" style={{ color: 'var(--text-secondary)' }}>{dbSize}</span>
</div>
<div className="flex items-center gap-2 px-2">
<div className="w-2 h-2 rounded-full" style={{ background: 'var(--accent)' }}></div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-semibold uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>Arch Linux OS</p>
<p className="text-[9px] font-mono truncate" style={{ color: 'var(--text-tertiary)' }}>Local Port: 5173</p>
</div>
</div>
</div>
</aside>
);
}
+91
View File
@@ -0,0 +1,91 @@
import {
Home,
Search,
Package,
ArrowDownToLine,
Grid3X3,
Settings,
Sun,
Moon,
} from 'lucide-react';
import { useTheme } from '../lib/theme';
import type { Page } from '../types';
interface SidebarProps {
activePage: Page;
onNavigate: (page: Page) => void;
}
const navItems: { id: Page; label: string; icon: typeof Home }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: Home },
{ id: 'search', label: 'Search Packages', icon: Search },
{ id: 'installed', label: 'Installed', icon: Package },
{ id: 'updates', label: 'Updates', icon: ArrowDownToLine },
{ id: 'categories', label: 'Categories', icon: Grid3X3 },
{ id: 'settings', label: 'Settings', icon: Settings },
];
export default function Sidebar({ activePage, onNavigate }: SidebarProps) {
const { theme, toggle } = useTheme();
const isDark = theme === 'dark';
return (
<aside
className={`w-[220px] flex-shrink-0 h-full flex flex-col border-r ${
isDark
? 'bg-dark-panel border-dark-border'
: 'bg-light-panel border-light-border'
}`}
>
{/* App title */}
<div
className={`px-4 py-3 border-b font-semibold text-sm flex items-center gap-2 ${
isDark ? 'border-dark-border' : 'border-light-border'
}`}
>
<Package size={18} className="text-arch-blue" />
<span>ArchStore</span>
</div>
{/* Navigation */}
<nav className="flex-1 py-1">
{navItems.map(({ id, label, icon: Icon }) => {
const active = activePage === id;
return (
<button
key={id}
onClick={() => onNavigate(id)}
className={`w-full flex items-center gap-2.5 px-4 py-1.5 text-sm text-left ${
active
? isDark
? 'bg-dark-active text-arch-blue'
: 'bg-light-active text-arch-blue'
: isDark
? 'text-dark-text-secondary hover:bg-dark-hover hover:text-dark-text'
: 'text-light-text-secondary hover:bg-light-hover hover:text-light-text'
}`}
>
<Icon size={16} />
{label}
</button>
);
})}
</nav>
{/* Theme toggle */}
<div className={`px-3 py-2 border-t ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<button
onClick={toggle}
className={`w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-sm ${
isDark
? 'text-dark-text-secondary hover:bg-dark-hover hover:text-dark-text'
: 'text-light-text-secondary hover:bg-light-hover hover:text-light-text'
}`}
>
{isDark ? <Sun size={14} /> : <Moon size={14} />}
{isDark ? 'Light Mode' : 'Dark Mode'}
</button>
</div>
</aside>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { useTheme } from '../lib/theme';
interface StatusBarProps {
backendStatus: 'connected' | 'disconnected' | 'checking';
installedCount: number;
updatesCount: number;
}
export default function StatusBar({ backendStatus, installedCount, updatesCount }: StatusBarProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const statusColor =
backendStatus === 'connected'
? 'bg-green-500'
: backendStatus === 'checking'
? 'bg-yellow-500'
: 'bg-red-500';
return (
<footer
className={`h-[24px] flex items-center px-3 text-[11px] gap-4 border-t flex-shrink-0 ${
isDark
? 'bg-dark-panel border-dark-border text-dark-text-secondary'
: 'bg-light-panel border-light-border text-light-text-secondary'
}`}
>
<span className="flex items-center gap-1.5">
<span className={`inline-block w-2 h-2 rounded-full ${statusColor}`} />
Backend: {backendStatus}
</span>
<span className={`w-px h-3 ${isDark ? 'bg-dark-border' : 'bg-light-border'}`} />
<span>Installed: {installedCount}</span>
<span className={`w-px h-3 ${isDark ? 'bg-dark-border' : 'bg-light-border'}`} />
<span>Updates: {updatesCount}</span>
<div className="flex-1" />
<span>ArchStore v1.0.0</span>
</footer>
);
}
+62
View File
@@ -0,0 +1,62 @@
import { Search, Download, RefreshCw, Bell } from 'lucide-react';
import { useTheme } from '../lib/theme';
import type { Page } from '../types';
interface ToolbarProps {
onNavigate: (page: Page) => void;
onSync: () => void;
updatesCount: number;
}
export default function Toolbar({ onNavigate, onSync, updatesCount }: ToolbarProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const btnClass = `flex items-center gap-1.5 px-3 py-1 text-xs border rounded-sm ${
isDark
? 'border-dark-border bg-dark-panel text-dark-text-secondary hover:bg-dark-hover hover:text-dark-text'
: 'border-light-border bg-light-panel text-light-text-secondary hover:bg-light-hover hover:text-light-text'
}`;
return (
<header
className={`h-[38px] flex items-center gap-2 px-3 border-b flex-shrink-0 ${
isDark
? 'bg-dark-panel border-dark-border'
: 'bg-light-panel border-light-border'
}`}
>
{/* Search shortcut */}
<button onClick={() => onNavigate('search')} className={btnClass}>
<Search size={13} />
Search Packages
</button>
<div className={`w-px h-5 ${isDark ? 'bg-dark-border' : 'bg-light-border'}`} />
{/* Quick actions */}
<button onClick={() => onNavigate('updates')} className={btnClass}>
<Download size={13} />
Updates
{updatesCount > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-[10px] font-medium bg-arch-blue text-white rounded-sm leading-none">
{updatesCount}
</span>
)}
</button>
<button onClick={onSync} className={btnClass}>
<RefreshCw size={13} />
Sync
</button>
{/* Spacer */}
<div className="flex-1" />
{/* Notifications */}
<button className={btnClass}>
<Bell size={13} />
</button>
</header>
);
}
+83 -732
View File
@@ -1,762 +1,113 @@
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import "tailwindcss";
/* ═══════════════════════════════════════════════
ArchStore — Premium Desktop App Design System
═══════════════════════════════════════════════ */
:root {
--bg-base: #0f1115;
--bg-primary: #141820;
--bg-secondary: #1a1f2a;
--bg-tertiary: #202636;
--bg-card: #131821;
--bg-card-hover: #131821;
--bg-elevated: #1a1f2a;
--bg-input: #141820;
--bg-sidebar: #0d1016;
--bg-overlay: rgba(0, 0, 0, 0.5);
--topbar-bg: #0f1218;
--border-primary: rgba(148, 163, 184, 0.12);
--border-secondary: rgba(148, 163, 184, 0.2);
--border-glow: rgba(56, 189, 248, 0.2);
--text-primary: #e2e8f0;
--text-secondary: #cbd5e1;
--text-tertiary: #94a3b8;
--text-inverse: #0f172a;
--accent: #38bdf8;
--accent-hover: #38bdf8;
--accent-muted: rgba(56, 189, 248, 0.12);
--accent-glow: rgba(56, 189, 248, 0.2);
--green: #22c55e;
--green-muted: rgba(34, 197, 94, 0.1);
--amber: #f59e0b;
--amber-muted: rgba(245, 158, 11, 0.12);
--red: #ef4444;
--red-muted: rgba(239, 68, 68, 0.12);
--blue: #60a5fa;
--blue-muted: rgba(96, 165, 250, 0.12);
--violet: #a78bfa;
--violet-muted: rgba(167, 139, 250, 0.12);
--shadow-sm: 0 1px 2px rgba(2, 6, 23, 0.25);
--shadow-md: 0 2px 6px rgba(2, 6, 23, 0.25);
--shadow-lg: 0 4px 10px rgba(2, 6, 23, 0.3);
--shadow-glow: 0 0 0 rgba(0, 0, 0, 0);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 10px;
--radius-xl: 12px;
--radius-2xl: 14px;
--radius-full: 9999px;
--font-sans: 'Outfit', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--transition-fast: 140ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 240ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 380ms cubic-bezier(0.4, 0, 0.2, 1);
}
.light {
--bg-base: #f0f0f1;
--bg-primary: #f6f7f7;
--bg-secondary: #ffffff;
--bg-tertiary: #f6f7f7;
--bg-card: #ffffff;
--bg-card-hover: #ffffff;
--bg-elevated: #ffffff;
--bg-input: #ffffff;
--bg-sidebar: #1d2327;
--bg-overlay: rgba(0, 0, 0, 0.2);
--topbar-bg: #ffffff;
--border-primary: #c3c4c7;
--border-secondary: #a7aaad;
--border-glow: rgba(34, 113, 177, 0.3);
--text-primary: #1d2327;
--text-secondary: #50575e;
--text-tertiary: #6c7781;
--text-inverse: #ffffff;
--accent: #2271b1;
--accent-hover: #2271b1;
--accent-muted: rgba(34, 113, 177, 0.12);
--accent-glow: rgba(34, 113, 177, 0.2);
--shadow-sm: 0 1px 1px rgba(0, 0, 0, 0.04);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 6px 18px rgba(0, 0, 0, 0.12);
--shadow-glow: 0 0 0 rgba(0, 0, 0, 0);
}
@theme {
--color-bg-base: var(--bg-base);
--color-bg-primary: var(--bg-primary);
--color-bg-secondary: var(--bg-secondary);
--color-bg-tertiary: var(--bg-tertiary);
--color-bg-card: var(--bg-card);
--color-bg-card-hover: var(--bg-card-hover);
--color-bg-elevated: var(--bg-elevated);
--color-bg-input: var(--bg-input);
--color-bg-sidebar: var(--bg-sidebar);
--color-bg-overlay: var(--bg-overlay);
--color-border-primary: var(--border-primary);
--color-border-secondary: var(--border-secondary);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
--color-text-tertiary: var(--text-tertiary);
--color-text-inverse: var(--text-inverse);
--color-accent: var(--accent);
--color-accent-hover: var(--accent-hover);
--color-accent-muted: var(--accent-muted);
--color-accent-glow: var(--accent-glow);
--color-green: var(--green);
--color-green-muted: var(--green-muted);
--color-amber: var(--amber);
--color-amber-muted: var(--amber-muted);
--color-red: var(--red);
--color-red-muted: var(--red-muted);
--color-blue: var(--blue);
--color-blue-muted: var(--blue-muted);
--color-violet: var(--violet);
--color-violet-muted: var(--violet-muted);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
/* Arch Linux blue accent */
--color-arch-blue: #1793D1;
--color-arch-blue-hover: #1480B8;
--color-arch-blue-light: #1793D120;
/* Dark mode palette */
--color-dark-bg: #000000;
--color-dark-panel: #111111;
--color-dark-border: #2A2A2A;
--color-dark-text: #FFFFFF;
--color-dark-text-secondary: #AAAAAA;
--color-dark-hover: #1A1A1A;
--color-dark-active: #222222;
/* Light mode palette */
--color-light-bg: #F4F4F4;
--color-light-panel: #FFFFFF;
--color-light-border: #D4D4D4;
--color-light-text: #111111;
--color-light-text-secondary: #666666;
--color-light-hover: #E8E8E8;
--color-light-active: #DDDDDD;
}
/* ── Reset ── */
/* Base reset */
*, *::before, *::after {
box-sizing: border-box;
}
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
animation: none !important;
transition: none !important;
}
body {
font-family: var(--font-sans);
background: var(--bg-base);
color: var(--text-primary);
font-size: 16px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.light body {
background: var(--bg-base);
/* Dark mode (default) */
body {
background-color: var(--color-dark-bg);
color: var(--color-dark-text);
}
/* ── Custom Scrollbar ── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-secondary); border-radius: 9999px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
/* ═══════════════════════════════════════════════
Desktop Shell Layout
═══════════════════════════════════════════════ */
.app-layout {
display: flex;
min-height: 100vh;
background-color: var(--bg-base);
overflow-x: hidden;
body.light {
background-color: var(--color-light-bg);
color: var(--color-light-text);
}
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
z-index: 50;
display: flex;
flex-direction: column;
padding: 22px 16px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-primary);
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: none;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.light .sidebar {
background: var(--bg-sidebar);
border-right: 1px solid #101517;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.main-content {
margin-left: 260px;
flex: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-base);
transition: none;
}
.content-shell {
width: 100%;
max-width: 100%;
margin: 0 auto;
padding: 0 28px 32px;
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.topbar {
position: sticky;
top: 0;
z-index: 40;
display: grid;
grid-template-columns: auto minmax(320px, 1fr) auto;
align-items: center;
gap: 16px;
padding: 14px 28px;
background: var(--topbar-bg);
border-bottom: 1px solid var(--border-primary);
box-shadow: none;
}
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.topbar-title {
display: flex;
flex-direction: column;
line-height: 1.1;
min-width: 0;
}
.topbar-title span:first-child {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.topbar-title span:last-child {
font-size: 12px;
color: var(--text-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.topbar-search {
flex: 1;
max-width: none;
min-width: 0;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.page-shell {
padding: 24px 0 32px;
flex: 1;
min-width: 0;
overflow-x: hidden;
}
@media (max-width: 1024px) {
.sidebar { transform: translateX(-100%); }
.sidebar.open { transform: translateX(0); }
.main-content { margin-left: 0; }
.content-shell { padding: 0 16px 24px; }
.topbar {
grid-template-columns: auto 1fr auto;
padding: 14px 20px;
}
}
.app-footer {
margin-top: auto;
padding: 18px 28px 22px;
text-align: center;
font-size: 12px;
color: var(--text-tertiary);
border-top: 1px solid var(--border-primary);
}
/* ── Premium Sidebar Nav Links ── */
.nav-link {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 12px;
border-radius: var(--radius-md);
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: none;
border: 1px solid transparent;
position: relative;
}
.nav-link span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.light .nav-link {
color: #c3c4c7;
}
.light .nav-link:hover {
color: #c3c4c7;
background: transparent;
border-color: transparent;
}
.light .nav-link.active {
color: #ffffff;
background: rgba(34, 113, 177, 0.2);
border-color: transparent;
box-shadow: none;
}
.light .nav-link.active::before {
background: var(--accent);
box-shadow: none;
}
.nav-link:hover { color: var(--text-secondary); background: transparent; border-color: transparent; }
.nav-link.active {
color: var(--text-primary);
background: rgba(56, 189, 248, 0.14);
border-color: var(--border-primary);
font-weight: 700;
box-shadow: none;
}
.nav-link.active::before {
content: '';
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--accent);
box-shadow: none;
}
/* ═══════════════════════════════════════════════
Glassmorphic Cards
═══════════════════════════════════════════════ */
.card {
background: var(--bg-card);
border: 1px solid rgba(148, 163, 184, 0.08);
border-radius: var(--radius-md);
box-shadow: none;
transition: none;
min-width: 0;
overflow: hidden;
}
.card-interactive { cursor: pointer; }
.card-interactive:hover {
background: var(--bg-card);
border-color: rgba(148, 163, 184, 0.08);
box-shadow: none;
transform: none;
}
/* ── Badges ── */
.badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Desktop Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 11px 20px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
font-family: var(--font-sans);
transition: none;
white-space: nowrap;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover:not(:disabled) { background: var(--accent); box-shadow: var(--shadow-sm); transform: none; }
.btn-secondary { background: var(--bg-secondary); color: var(--text-secondary); border-color: var(--border-primary); }
.btn-secondary:hover:not(:disabled) { border-color: var(--border-primary); color: var(--text-secondary); }
.btn-danger { background: var(--red-muted); color: var(--red); border-color: rgba(239, 68, 68, 0.2); }
.btn-danger:hover:not(:disabled) { background: var(--red-muted); }
.btn-ghost { background: transparent; color: var(--text-tertiary); }
.btn-ghost:hover:not(:disabled) { color: var(--text-tertiary); background: transparent; }
/* ── Form Inputs ── */
.input {
width: 100%;
padding: 14px 18px;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 14px;
outline: none;
transition: none;
}
.light .input:focus {
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.2);
}
.input::placeholder { color: var(--text-tertiary); }
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.searchbar { width: 100%; position: relative; }
.input-search { height: 46px; font-size: 14px; }
/* ── Typography & Headings ── */
.page-title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--text-primary);
line-height: 1.2;
}
.page-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-top: 6px;
}
/* ── Custom Component Grids ── */
.pkg-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
align-items: stretch;
}
.package-card {
height: 100%;
}
@media (max-width: 640px) { .pkg-grid { grid-template-columns: 1fr; } }
.cat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 22px;
}
.update-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
transition: none;
}
.update-row:hover { background: transparent; }
/* ── Animation Shimmers ── */
.shimmer {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
}
.spinner {
width: 24px; height: 24px;
border: 2px solid var(--border-primary);
border-top-color: var(--accent);
border-radius: 50%;
}
.theme-toggle {
width: 42px;
height: 42px;
border-radius: var(--radius-md);
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: none;
}
.theme-toggle:hover { border-color: var(--border-primary); color: var(--text-secondary); box-shadow: none; }
.avatar-button {
width: 42px;
height: 42px;
border-radius: 14px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
letter-spacing: 0.04em;
font-size: 12px;
box-shadow: var(--shadow-sm);
}
.gradient-text {
color: var(--accent);
}
/* ── Premium Utility Blocks ── */
.glass-panel {
background: transparent;
border: none;
border-radius: 0;
}
.light .glass-panel {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(244, 247, 251, 0.9));
border: 1px solid var(--border-primary);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.section-kicker {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--accent);
}
.section-title {
font-size: 20px;
font-weight: 800;
color: var(--text-primary);
}
.section-subtitle {
font-size: 13px;
color: var(--text-tertiary);
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.kpi-card {
padding: 16px;
border-radius: var(--radius-lg);
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
}
.light .kpi-card {
background: rgba(255, 255, 255, 0.9);
}
.kpi-value {
font-size: 22px;
font-weight: 800;
color: var(--text-primary);
}
.kpi-label {
font-size: 12px;
color: var(--text-tertiary);
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
border: 1px solid var(--border-primary);
background: rgba(15, 23, 42, 0.7);
}
.status-dot {
/* Scrollbar styling - classic thin scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
border-radius: 999px;
}
.progress-track {
::-webkit-scrollbar-track {
background: var(--color-dark-panel);
}
::-webkit-scrollbar-thumb {
background: var(--color-dark-border);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}
body.light ::-webkit-scrollbar-track {
background: var(--color-light-bg);
}
body.light ::-webkit-scrollbar-thumb {
background: var(--color-light-border);
}
/* Selection */
::selection {
background-color: var(--color-arch-blue);
color: white;
}
/* Focus outline - classic dotted */
:focus-visible {
outline: 1px dotted var(--color-arch-blue);
outline-offset: 1px;
}
/* Table styles */
table {
border-collapse: collapse;
width: 100%;
height: 8px;
background: rgba(148, 163, 184, 0.12);
border-radius: 999px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 999px;
background: var(--accent);
/* Classic button reset */
button {
cursor: pointer;
font-family: inherit;
}
.data-table {
width: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--border-primary);
background: var(--bg-card);
min-width: 0;
/* Input reset */
input, select {
font-family: inherit;
}
@media (max-width: 1024px) {
.data-table {
overflow-x: auto;
}
}
img, svg {
max-width: 100%;
height: auto;
}
.table-head,
.table-row {
display: grid;
grid-template-columns: 2.5fr 1.2fr 1fr 1.4fr 0.8fr;
gap: 16px;
align-items: center;
padding: 14px 18px;
min-width: 0;
}
.table-head span,
.table-row span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-head {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--text-tertiary);
background: rgba(15, 23, 42, 0.85);
border-bottom: 1px solid var(--border-primary);
}
.table-row {
font-size: 13px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-primary);
transition: none;
}
.table-row:hover { background: transparent; }
.table-row:last-child {
border-bottom: none;
}
@media (max-width: 1024px) {
.table-head,
.table-row {
grid-template-columns: 2fr 1fr 1fr;
}
.table-head span:nth-child(4),
.table-head span:nth-child(5),
.table-row span:nth-child(4),
.table-row span:nth-child(5) {
display: none;
}
}
.toggle {
position: relative;
width: 42px;
height: 24px;
border-radius: 999px;
background: rgba(148, 163, 184, 0.25);
border: 1px solid var(--border-primary);
display: inline-flex;
align-items: center;
padding: 2px;
transition: none;
}
.toggle::after {
content: '';
width: 18px;
height: 18px;
border-radius: 999px;
background: #fff;
transition: none;
}
.toggle.is-on {
background: var(--accent);
border-color: transparent;
}
.toggle.is-on::after {
transform: translateX(18px);
}
/* ── Animations ── */
.animate-fade-in { animation: none; }
.animate-slide-up { animation: none; }
.stagger > * { animation: none; }
.animate-pulse { animation: none !important; }
-132
View File
@@ -1,132 +0,0 @@
import { useState, useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import Sidebar from '../components/Sidebar';
import SearchBar from '../components/SearchBar';
import { Menu, X, Sun, Moon, RefreshCw, Rocket, TerminalSquare, Bell } from 'lucide-react';
import api from '../api/client';
export default function MainLayout() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [theme, setTheme] = useState(() => localStorage.getItem('archstore-theme') || 'dark');
const [checking, setChecking] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const root = document.documentElement;
if (theme === 'light') root.classList.add('light');
else root.classList.remove('light');
localStorage.setItem('archstore-theme', theme);
}, [theme]);
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
const handleSearch = (query) => {
if (query.trim()) {
navigate(`/search?q=${encodeURIComponent(query.trim())}`);
setSidebarOpen(false);
}
};
const handleSync = async () => {
setChecking(true);
try {
await api.clearCache();
alert('Local package metadata cache synchronized successfully!');
} catch (e) {
alert('Error: ' + e.message);
} finally {
setChecking(false);
}
};
return (
<div className="app-layout">
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 lg:hidden"
style={{ background: 'var(--bg-overlay)' }}
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar Navigation */}
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
{/* Desktop Main Content Shell */}
<main className="main-content">
{/* Full-width premium Topbar */}
<header className="topbar">
<div className="topbar-left">
<button
className="btn-ghost lg:hidden !p-2 rounded-xl"
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label="Toggle menu"
>
{sidebarOpen ? <X size={18} /> : <Menu size={18} />}
</button>
<div className="topbar-title">
<span>ArchStore</span>
<span>Pacman + AUR workspace</span>
</div>
</div>
{/* Large desktop Search input container */}
<div className="topbar-search">
<SearchBar onSearch={handleSearch} />
</div>
{/* Quick Header Actions */}
<div className="topbar-actions">
<button
className="btn btn-secondary !py-2.5 !px-4 text-sm hidden md:flex items-center gap-2"
onClick={() => navigate('/search?q=')}
title="New Search"
>
<Rocket size={13} /> Quick Install
</button>
<button
className="btn btn-secondary !py-2.5 !px-3 text-sm hidden md:flex items-center gap-2"
onClick={() => navigate('/updates')}
title="Check Updates"
>
<TerminalSquare size={13} /> Updates
</button>
<button
onClick={handleSync}
disabled={checking}
className="btn btn-secondary !py-2.5 !px-4 text-sm hidden sm:flex items-center gap-2"
title="Synchronize Local Cache"
>
<RefreshCw size={13} className={checking ? 'animate-spin' : ''} />
<span>Sync</span>
</button>
<button className="btn-ghost !p-2 rounded-xl" title="Notifications" aria-label="Notifications">
<Bell size={16} />
</button>
{/* Premium Theme Switch Toggle */}
<button onClick={toggleTheme} className="theme-toggle" aria-label="Toggle theme">
{theme === 'dark' ? <Sun size={15} /> : <Moon size={15} />}
</button>
{/* Profile */}
<button className="avatar-button" aria-label="Open profile">
AS
</button>
</div>
</header>
{/* Constrained layout shell */}
<div className="content-shell">
<div className="page-shell">
<Outlet />
</div>
</div>
<footer className="app-footer">
Made with for Arch Linux
</footer>
</main>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
import type {
SearchResult,
InstalledResult,
UpdatesResult,
CategoriesResult,
CategoryPackagesResult,
Package,
ScanResult,
} from '../types';
const BASE = '/api';
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || 'Request failed');
}
return res.json();
}
export const api = {
// Packages
searchPackages: (query: string, source = 'all') =>
request<SearchResult>(`/packages/search?q=${encodeURIComponent(query)}&source=${source}`),
getPackageInfo: (name: string) =>
request<Package>(`/packages/${encodeURIComponent(name)}`),
listInstalled: () =>
request<InstalledResult>('/packages/installed'),
installPackage: (name: string) =>
request<{ success: boolean; message: string }>(`/packages/${encodeURIComponent(name)}/install`, { method: 'POST' }),
removePackage: (name: string) =>
request<{ success: boolean; message: string }>(`/packages/${encodeURIComponent(name)}/remove`, { method: 'POST' }),
scanPackage: (name: string) =>
request<ScanResult>(`/packages/${encodeURIComponent(name)}/scan`),
// Updates
checkUpdates: () =>
request<UpdatesResult>('/updates/check'),
applyUpdates: () =>
request<{ success: boolean; message: string }>('/updates/apply', { method: 'POST' }),
// Categories
listCategories: () =>
request<CategoriesResult>('/categories'),
getCategoryPackages: (name: string) =>
request<CategoryPackagesResult>(`/categories/${encodeURIComponent(name)}`),
// Health
health: () =>
request<{ status: string; database: string; version: string }>('/health'),
// Cache
clearCache: () =>
request<{ status: string; message: string }>('/cache/clear', { method: 'POST' }),
};
+38
View File
@@ -0,0 +1,38 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import type { ThemeMode } from '../types';
interface ThemeContextType {
theme: ThemeMode;
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: 'dark',
toggle: () => {},
});
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<ThemeMode>(() => {
const saved = localStorage.getItem('archstore-theme');
return (saved as ThemeMode) || 'dark';
});
useEffect(() => {
localStorage.setItem('archstore-theme', theme);
if (theme === 'light') {
document.body.classList.add('light');
} else {
document.body.classList.remove('light');
}
}, [theme]);
const toggle = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
-10
View File
@@ -1,10 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+13
View File
@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ThemeProvider } from './lib/theme'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>,
)
-126
View File
@@ -1,126 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import api from '../api/client';
import PackageGrid from '../components/PackageGrid';
import {
Code, Monitor, Wifi, Music, Gamepad2, LayoutDashboard, Type, ShieldCheck,
ArrowLeft, Package
} from 'lucide-react';
const catMeta = {
Development: { icon: Code, color: '#60a5fa', bg: 'rgba(96,165,250,0.08)', desc: 'Programming tools, compilers, and IDEs' },
System: { icon: Monitor, color: '#94a3b8', bg: 'rgba(148,163,184,0.08)', desc: 'Core system utilities and tools' },
Network: { icon: Wifi, color: '#34d399', bg: 'rgba(52,211,153,0.08)', desc: 'Networking tools, browsers, and servers' },
Multimedia: { icon: Music, color: '#c084fc', bg: 'rgba(192,132,252,0.08)', desc: 'Audio, video, and image tools' },
Games: { icon: Gamepad2, color: '#f87171', bg: 'rgba(248,113,113,0.08)', desc: 'Games and gaming tools' },
Desktop: { icon: LayoutDashboard, color: '#818cf8', bg: 'rgba(129,140,248,0.08)', desc: 'Desktop environments and window managers' },
Fonts: { icon: Type, color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', desc: 'Fonts and typography' },
Security: { icon: ShieldCheck, color: '#22d3ee', bg: 'rgba(34,211,238,0.08)', desc: 'Security and privacy tools' },
};
export default function Categories() {
const { categoryName } = useParams();
const navigate = useNavigate();
const [categories, setCategories] = useState([]);
const [packages, setPackages] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (categoryName) loadPkgs(categoryName);
else loadCats();
}, [categoryName]);
async function loadCats() {
setLoading(true); setError(null);
try { const d = await api.listCategories(); setCategories(d.results || []); }
catch (e) { setError(e.message); }
finally { setLoading(false); }
}
async function loadPkgs(name) {
setLoading(true); setError(null);
try { const d = await api.getCategoryPackages(name); setPackages(d.results || []); }
catch (e) { setError(e.message); }
finally { setLoading(false); }
}
/* ── Category detail view ── */
if (categoryName) {
const meta = catMeta[categoryName] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)' };
const Icon = meta.icon;
return (
<div className="animate-slide-up flex flex-col gap-6">
<button onClick={() => navigate('/categories')} className="btn btn-secondary w-fit text-xs">
<ArrowLeft size={12} /> Back
</button>
<div className="card p-6 flex flex-col lg:flex-row lg:items-center gap-4">
<div className="w-12 h-12 rounded-2xl flex items-center justify-center"
style={{ background: meta.bg, border: '1px solid var(--border-primary)' }}>
<Icon size={22} style={{ color: meta.color }} />
</div>
<div className="flex-1">
<h1 className="page-title">{categoryName}</h1>
<p className="page-subtitle">Browse popular {categoryName.toLowerCase()} packages</p>
</div>
<div className="pill">{packages.length} packages</div>
</div>
{error && <div className="rounded-lg p-3 text-xs" style={{ background: 'var(--red-muted)', color: 'var(--red)' }}>{error}</div>}
<PackageGrid packages={packages} loading={loading} />
</div>
);
}
/* ── Category list view ── */
return (
<div className="animate-slide-up flex flex-col gap-6">
<div>
<h1 className="page-title">Categories</h1>
<p className="page-subtitle">Explore software by type</p>
</div>
{error && <div className="rounded-lg p-3 text-xs" style={{ background: 'var(--red-muted)', color: 'var(--red)' }}>{error}</div>}
{loading ? (
<div className="cat-grid">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="card p-4">
<div className="flex items-center gap-3">
<div className="shimmer w-10 h-10 rounded-xl"></div>
<div className="flex-1">
<div className="shimmer h-3 w-28 mb-2"></div>
<div className="shimmer h-2.5 w-full"></div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger">
{(categories.length > 0 ? categories : Object.keys(catMeta).map(n => ({ name: n }))).map((cat) => {
const meta = catMeta[cat.name] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)', desc: 'Explore packages' };
const Icon = meta.icon;
return (
<div key={cat.name}
className="card card-interactive p-5 group"
onClick={() => navigate(`/categories/${cat.name}`)}>
<div className="flex items-start gap-3">
<div className="w-11 h-11 rounded-2xl flex items-center justify-center shrink-0 transition-transform group-hover:scale-110"
style={{ background: meta.bg, border: '1px solid rgba(255,255,255,0.05)' }}>
<Icon size={18} style={{ color: meta.color }} />
</div>
<div className="min-w-0">
<p className="text-sm font-bold mb-1" style={{ color: 'var(--text-primary)' }}>{cat.name}</p>
<p className="text-[11px] leading-snug" style={{ color: 'var(--text-tertiary)' }}>
{cat.description || meta.desc}
</p>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
}
+165
View File
@@ -0,0 +1,165 @@
import { useEffect, useState } from 'react';
import {
Code2,
Cpu,
Globe,
Music,
Gamepad2,
Monitor,
Type,
ShieldCheck,
ChevronRight,
} from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { Package } from '../types';
interface CategoriesProps {
onSelectPackage: (name: string) => void;
}
const categoryIcons: Record<string, typeof Code2> = {
Development: Code2,
System: Cpu,
Network: Globe,
Multimedia: Music,
Games: Gamepad2,
Desktop: Monitor,
Fonts: Type,
Security: ShieldCheck,
};
const defaultCategories = [
{ name: 'Development', description: 'Programming tools, compilers, IDEs, and libraries' },
{ name: 'System', description: 'System utilities, shells, and administration tools' },
{ name: 'Network', description: 'Network tools, browsers, and communication' },
{ name: 'Multimedia', description: 'Audio, video, and image applications' },
{ name: 'Games', description: 'Games and entertainment software' },
{ name: 'Desktop', description: 'Desktop environments, window managers, and themes' },
{ name: 'Fonts', description: 'Font packages and typefaces' },
{ name: 'Security', description: 'Security tools, encryption, and firewalls' },
];
export default function Categories({ onSelectPackage }: CategoriesProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [activeCategory, setActiveCategory] = useState<string | null>(null);
const [packages, setPackages] = useState<Package[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (activeCategory) {
setLoading(true);
api.getCategoryPackages(activeCategory)
.then(data => setPackages(data.results))
.catch(() => setPackages([]))
.finally(() => setLoading(false));
}
}, [activeCategory]);
const panelClass = `border rounded-sm ${
isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'
}`;
const secondaryText = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Categories
</h1>
{/* Category grid */}
<div className="grid grid-cols-4 gap-3">
{defaultCategories.map(cat => {
const Icon = categoryIcons[cat.name] || Code2;
const active = activeCategory === cat.name;
return (
<button
key={cat.name}
onClick={() => setActiveCategory(active ? null : cat.name)}
className={`flex items-start gap-3 p-3 text-left border rounded-sm ${
active
? 'border-arch-blue bg-arch-blue-light'
: isDark
? 'border-dark-border hover:bg-dark-hover bg-dark-panel'
: 'border-light-border hover:bg-light-hover bg-light-panel'
}`}
>
<Icon size={20} className={active ? 'text-arch-blue' : secondaryText} />
<div>
<div className={`text-sm font-medium ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
{cat.name}
</div>
<div className={`text-xs mt-0.5 ${secondaryText}`}>
{cat.description}
</div>
</div>
</button>
);
})}
</div>
{/* Category packages */}
{activeCategory && (
<div className={panelClass}>
<div className={`px-3 py-2 border-b flex items-center gap-1.5 ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<ChevronRight size={14} className="text-arch-blue" />
<span className={`text-sm font-medium ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
{activeCategory}
</span>
<span className={`text-xs ${secondaryText}`}>
({packages.length} packages)
</span>
</div>
{loading ? (
<div className={`p-4 text-sm ${secondaryText}`}>Loading packages...</div>
) : packages.length === 0 ? (
<div className={`p-4 text-sm text-center ${secondaryText}`}>
No packages found in this category.
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="px-3 py-1.5 font-medium">Package</th>
<th className="px-3 py-1.5 font-medium">Description</th>
<th className="px-3 py-1.5 font-medium">Version</th>
<th className="px-3 py-1.5 font-medium">Status</th>
</tr>
</thead>
<tbody>
{packages.map(pkg => (
<tr
key={pkg.name}
className={`border-t cursor-pointer ${
isDark
? 'border-dark-border hover:bg-dark-hover'
: 'border-light-border hover:bg-light-hover'
}`}
onClick={() => onSelectPackage(pkg.name)}
>
<td className="px-3 py-1.5 text-arch-blue font-medium">{pkg.name}</td>
<td className={`px-3 py-1.5 ${secondaryText} max-w-[350px] truncate`}>
{pkg.description}
</td>
<td className={`px-3 py-1.5 text-xs font-mono ${secondaryText}`}>
{pkg.version}
</td>
<td className="px-3 py-1.5">
{pkg.installed ? (
<span className="text-xs text-green-500">Installed</span>
) : (
<span className={`text-xs ${secondaryText}`}>Available</span>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
);
}
+297
View File
@@ -0,0 +1,297 @@
import { useEffect, useState } from 'react';
import {
Search,
ArrowDownToLine,
Package,
Grid3X3,
Clock,
ChevronRight,
} from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { Page, UpdatePackage } from '../types';
interface DashboardProps {
onNavigate: (page: Page) => void;
onSelectPackage: (name: string) => void;
}
export default function Dashboard({ onNavigate, onSelectPackage }: DashboardProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [updates, setUpdates] = useState<UpdatePackage[]>([]);
const [quickSearch, setQuickSearch] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
api.checkUpdates()
.then(data => setUpdates(data.results))
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const panelClass = `border rounded-sm ${
isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'
}`;
const sectionTitle = `text-lg font-medium mb-2 ${isDark ? 'text-dark-text' : 'text-light-text'}`;
const secondaryText = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const popularPackages = [
{ name: 'firefox', desc: 'Standalone web browser from Mozilla', repo: 'extra' },
{ name: 'vim', desc: 'Highly configurable text editor', repo: 'extra' },
{ name: 'git', desc: 'Distributed version control system', repo: 'extra' },
{ name: 'htop', desc: 'Interactive process viewer', repo: 'extra' },
{ name: 'neovim', desc: 'Vim-fork focused on extensibility', repo: 'extra' },
{ name: 'docker', desc: 'Container runtime environment', repo: 'extra' },
{ name: 'nodejs', desc: 'Evented I/O for V8 javascript', repo: 'extra' },
{ name: 'python', desc: 'High-level programming language', repo: 'extra' },
];
const categories = [
{ name: 'Development', icon: '⌨' },
{ name: 'System', icon: '⚙' },
{ name: 'Network', icon: '🌐' },
{ name: 'Multimedia', icon: '🎵' },
{ name: 'Games', icon: '🎮' },
{ name: 'Desktop', icon: '🖥' },
];
const recentActivity = [
{ action: 'Installed', pkg: 'neovim', time: '2 hours ago' },
{ action: 'Updated', pkg: 'firefox', time: '5 hours ago' },
{ action: 'Removed', pkg: 'nano', time: '1 day ago' },
{ action: 'Installed', pkg: 'ripgrep', time: '2 days ago' },
{ action: 'Updated', pkg: 'linux', time: '3 days ago' },
];
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Dashboard
</h1>
{/* Quick search */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<h2 className={sectionTitle} style={{ marginBottom: 0 }}>Search Packages</h2>
</div>
<div className="p-3 flex gap-2">
<div className="relative flex-1">
<Search size={14} className={`absolute left-2.5 top-1/2 -translate-y-1/2 ${secondaryText}`} />
<input
type="text"
placeholder="Search pacman and AUR packages..."
value={quickSearch}
onChange={e => setQuickSearch(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && quickSearch.trim()) {
onNavigate('search');
}
}}
className={`w-full pl-8 pr-3 py-1.5 text-sm border rounded-sm ${
isDark
? 'bg-dark-bg border-dark-border text-dark-text placeholder:text-dark-text-secondary'
: 'bg-light-bg border-light-border text-light-text placeholder:text-light-text-secondary'
}`}
/>
</div>
<button
onClick={() => onNavigate('search')}
className="px-4 py-1.5 text-sm bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover"
>
Search
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Updates available */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b flex items-center justify-between ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<h2 className={sectionTitle} style={{ marginBottom: 0 }}>
<ArrowDownToLine size={16} className="inline mr-1.5 text-arch-blue" />
Updates Available
</h2>
<button
onClick={() => onNavigate('updates')}
className={`text-xs ${secondaryText} hover:text-arch-blue flex items-center gap-0.5`}
>
View All <ChevronRight size={12} />
</button>
</div>
<div className="p-3">
{loading ? (
<p className={`text-sm ${secondaryText}`}>Checking for updates...</p>
) : updates.length === 0 ? (
<p className={`text-sm ${secondaryText}`}>System is up to date.</p>
) : (
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="pb-1 font-medium">Package</th>
<th className="pb-1 font-medium">Current</th>
<th className="pb-1 font-medium">New</th>
<th className="pb-1 font-medium">Source</th>
</tr>
</thead>
<tbody>
{updates.slice(0, 5).map(u => (
<tr key={u.name} className={`border-t ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<td className="py-1 text-arch-blue cursor-pointer hover:underline" onClick={() => onSelectPackage(u.name)}>
{u.name}
</td>
<td className={`py-1 ${secondaryText}`}>{u.current_version}</td>
<td className="py-1">{u.new_version}</td>
<td className="py-1">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
u.source === 'aur'
? 'bg-arch-blue-light text-arch-blue'
: isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'
}`}>
{u.source}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
{updates.length > 5 && (
<p className={`text-xs mt-2 ${secondaryText}`}>
and {updates.length - 5} more...
</p>
)}
</div>
</div>
{/* Recent activity */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<h2 className={sectionTitle} style={{ marginBottom: 0 }}>
<Clock size={16} className="inline mr-1.5 text-arch-blue" />
Recent Activity
</h2>
</div>
<div className="p-3">
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="pb-1 font-medium">Action</th>
<th className="pb-1 font-medium">Package</th>
<th className="pb-1 font-medium">Time</th>
</tr>
</thead>
<tbody>
{recentActivity.map((a, i) => (
<tr key={i} className={`border-t ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<td className="py-1">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
a.action === 'Installed'
? 'bg-green-900/30 text-green-400'
: a.action === 'Updated'
? 'bg-arch-blue-light text-arch-blue'
: 'bg-red-900/30 text-red-400'
}`}>
{a.action}
</span>
</td>
<td className="py-1 text-arch-blue cursor-pointer hover:underline" onClick={() => onSelectPackage(a.pkg)}>
{a.pkg}
</td>
<td className={`py-1 ${secondaryText}`}>{a.time}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Popular packages */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<h2 className={sectionTitle} style={{ marginBottom: 0 }}>
<Package size={16} className="inline mr-1.5 text-arch-blue" />
Popular Packages
</h2>
</div>
<div className="p-0">
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="px-3 py-1.5 font-medium">Name</th>
<th className="px-3 py-1.5 font-medium">Description</th>
<th className="px-3 py-1.5 font-medium">Repository</th>
<th className="px-3 py-1.5 font-medium w-20"></th>
</tr>
</thead>
<tbody>
{popularPackages.map(pkg => (
<tr
key={pkg.name}
className={`border-t cursor-pointer ${
isDark
? 'border-dark-border hover:bg-dark-hover'
: 'border-light-border hover:bg-light-hover'
}`}
onClick={() => onSelectPackage(pkg.name)}
>
<td className="px-3 py-1.5 text-arch-blue font-medium">{pkg.name}</td>
<td className={`px-3 py-1.5 ${secondaryText}`}>{pkg.desc}</td>
<td className="px-3 py-1.5">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'
}`}>
{pkg.repo}
</span>
</td>
<td className="px-3 py-1.5">
<button
onClick={e => { e.stopPropagation(); onSelectPackage(pkg.name); }}
className="px-2 py-0.5 text-xs bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover"
>
View
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Categories */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b flex items-center justify-between ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<h2 className={sectionTitle} style={{ marginBottom: 0 }}>
<Grid3X3 size={16} className="inline mr-1.5 text-arch-blue" />
Categories
</h2>
<button
onClick={() => onNavigate('categories')}
className={`text-xs ${secondaryText} hover:text-arch-blue flex items-center gap-0.5`}
>
View All <ChevronRight size={12} />
</button>
</div>
<div className="p-3 grid grid-cols-6 gap-2">
{categories.map(cat => (
<button
key={cat.name}
onClick={() => onNavigate('categories')}
className={`flex items-center gap-2 px-3 py-2 text-sm border rounded-sm text-left ${
isDark
? 'border-dark-border hover:bg-dark-hover text-dark-text'
: 'border-light-border hover:bg-light-hover text-light-text'
}`}
>
<span>{cat.icon}</span>
{cat.name}
</button>
))}
</div>
</div>
</div>
);
}
-318
View File
@@ -1,318 +0,0 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Search, Package, ArrowRight, Shield,
Code, Monitor, Wifi, Music, Gamepad2, LayoutDashboard, Type, ShieldCheck,
Download, RefreshCw, Zap, Cpu, HardDrive, AlertTriangle
} from 'lucide-react';
import api from '../api/client';
import PackageGrid from '../components/PackageGrid';
const catMeta = {
Development: { icon: Code, color: '#38bdf8', bg: 'rgba(56, 189, 248, 0.07)', gradient: 'linear-gradient(135deg, rgba(56, 189, 248, 0.15) 0%, transparent 100%)' },
System: { icon: Monitor, color: '#94a3b8', bg: 'rgba(148, 163, 184, 0.07)', gradient: 'linear-gradient(135deg, rgba(148, 163, 184, 0.15) 0%, transparent 100%)' },
Network: { icon: Wifi, color: '#34d399', bg: 'rgba(52, 211, 153, 0.07)', gradient: 'linear-gradient(135deg, rgba(52, 211, 153, 0.15) 0%, transparent 100%)' },
Multimedia: { icon: Music, color: '#c084fc', bg: 'rgba(192, 132, 252, 0.07)', gradient: 'linear-gradient(135deg, rgba(192, 132, 252, 0.15) 0%, transparent 100%)' },
Games: { icon: Gamepad2, color: '#f87171', bg: 'rgba(248, 113, 113, 0.07)', gradient: 'linear-gradient(135deg, rgba(248, 113, 113, 0.15) 0%, transparent 100%)' },
Desktop: { icon: LayoutDashboard, color: '#818cf8', bg: 'rgba(129, 140, 248, 0.07)', gradient: 'linear-gradient(135deg, rgba(129, 140, 248, 0.15) 0%, transparent 100%)' },
Fonts: { icon: Type, color: '#fbbf24', bg: 'rgba(251, 191, 36, 0.07)', gradient: 'linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, transparent 100%)' },
Security: { icon: ShieldCheck, color: '#22d3ee', bg: 'rgba(34, 211, 238, 0.07)', gradient: 'linear-gradient(135deg, rgba(34, 211, 238, 0.15) 0%, transparent 100%)' },
};
export default function Home() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [featured, setFeatured] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [updateCount, setUpdateCount] = useState(0);
const [sysMetrics, setSysMetrics] = useState({ cpu: '3.4%', ram: '2.8 GB', disk: '18% available' });
const quickInstalls = ['neovim', 'kitty', 'fastfetch', 'btop', 'wezterm', 'starship'];
useEffect(() => {
loadData();
// Simulate slight metrics variation for a rich dashboard visual experience
const timer = setInterval(() => {
setSysMetrics(prev => ({
cpu: `${(Math.random() * 5 + 2).toFixed(1)}%`,
ram: `${(Math.random() * 0.3 + 2.6).toFixed(2)} GB / 16 GB`,
disk: '62.4 GB free'
}));
}, 4000);
return () => clearInterval(timer);
}, []);
async function loadData() {
setLoading(true);
try {
const [catRes, featRes, updRes] = await Promise.allSettled([
api.listCategories(),
api.searchPackages('firefox chromium vlc neovim git kitty', 'all'),
api.checkUpdates(),
]);
if (catRes.status === 'fulfilled') setCategories(catRes.value.results || []);
if (featRes.status === 'fulfilled') setFeatured((featRes.value.results || []).slice(0, 6));
if (updRes.status === 'fulfilled') setUpdateCount(updRes.value.count || 0);
} catch { /* silent */ }
finally { setLoading(false); }
}
const handleSearch = (e) => {
e.preventDefault();
if (query.trim()) navigate(`/search?q=${encodeURIComponent(query.trim())}`);
};
return (
<div className="animate-slide-up grid grid-cols-1 xl:grid-cols-12 gap-8">
{/* ── Left Main Panel ── */}
<div className="xl:col-span-8 flex flex-col gap-8 min-w-0">
{/* Hero Banner */}
<section className="card p-8 min-h-[260px]">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
<div className="flex flex-col gap-4">
<span className="section-kicker">Unified Package Command Center</span>
<h1 className="text-4xl font-extrabold leading-tight" style={{ letterSpacing: '-0.03em' }}>
Command your <span className="gradient-text">Arch Linux</span> ecosystem
</h1>
<p className="text-sm max-w-xl" style={{ color: 'var(--text-secondary)' }}>
Orchestrate pacman repositories and AUR workflows with security insight, real-time system signals, and guided installs.
</p>
<form onSubmit={handleSearch} className="flex flex-col sm:flex-row gap-3 mt-2">
<div className="relative flex-1">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2" style={{ color: 'var(--text-tertiary)' }} />
<input
type="text"
className="input pl-11 pr-4 rounded-xl"
placeholder="Search repository or AUR..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<button type="submit" className="btn btn-primary px-6 rounded-xl">
<Search size={15} /> Search
</button>
</form>
</div>
<div className="glass-panel p-6 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em]" style={{ color: 'var(--text-tertiary)' }}>System Status</p>
<p className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>Arch Linux / Kernel 6.x</p>
</div>
<div className="pill">
<span className="status-dot" style={{ background: 'var(--green)' }}></span>
Stable
</div>
</div>
<div className="kpi-grid">
<div className="kpi-card">
<p className="kpi-label">CPU Load</p>
<p className="kpi-value">{sysMetrics.cpu}</p>
</div>
<div className="kpi-card">
<p className="kpi-label">Memory</p>
<p className="kpi-value">{sysMetrics.ram}</p>
</div>
<div className="kpi-card">
<p className="kpi-label">Storage</p>
<p className="kpi-value">{sysMetrics.disk}</p>
</div>
<div className="kpi-card">
<p className="kpi-label">Updates</p>
<p className="kpi-value">{updateCount}</p>
</div>
</div>
</div>
</div>
</section>
{/* Quick Install and Trending */}
<section className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="card p-6 lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<div>
<p className="section-title">Trending Packages</p>
<p className="section-subtitle">Top installs across pacman + AUR</p>
</div>
<button className="btn-ghost text-xs font-semibold flex items-center gap-1.5"
style={{ color: 'var(--accent)' }}
onClick={() => navigate('/search?q=popular')}>
Explore more <ArrowRight size={14} />
</button>
</div>
<div className="flex gap-4 overflow-x-auto pb-2">
{(featured.length > 0 ? featured : []).map((pkg) => (
<div key={`${pkg.source}-${pkg.name}`} className="card p-4 min-w-[240px]">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-sm" style={{ color: 'var(--text-primary)' }}>{pkg.name}</span>
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px]`}>
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
</span>
</div>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>{pkg.description || 'No description available.'}</p>
<button className="btn btn-secondary text-xs mt-3" onClick={() => navigate(`/package/${pkg.name}`)}>
View details
</button>
</div>
))}
</div>
</div>
<div className="card p-6">
<div className="flex items-center gap-2 mb-3">
<Download size={16} style={{ color: 'var(--accent)' }} />
<p className="section-title">Quick Install</p>
</div>
<div className="flex flex-wrap gap-2">
{quickInstalls.map((item) => (
<button key={item} className="pill" onClick={() => navigate(`/search?q=${item}`)}>
{item}
</button>
))}
</div>
</div>
</section>
{/* Categories Section */}
<section>
<div className="flex items-center justify-between mb-4">
<div>
<p className="section-title">Curated Categories</p>
<p className="section-subtitle">Focused collections for professional workflows</p>
</div>
<button className="btn-ghost text-xs font-semibold flex items-center gap-1.5"
style={{ color: 'var(--accent)' }}
onClick={() => navigate('/categories')}>
View all categories <ArrowRight size={14} />
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 stagger">
{(categories.length > 0 ? categories.slice(0, 8) : Object.keys(catMeta).map(n => ({ name: n }))).map((cat) => {
const meta = catMeta[cat.name] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)', gradient: 'none' };
const Icon = meta.icon;
return (
<div key={cat.name}
className="card card-interactive p-4 flex flex-col gap-3 group relative overflow-hidden"
style={{ background: meta.bg, backgroundImage: meta.gradient }}
onClick={() => navigate(`/categories/${cat.name}`)}>
<div className="w-9 h-9 rounded-xl flex items-center justify-center shrink-0 transition-all duration-300 group-hover:scale-110"
style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
<Icon size={17} style={{ color: meta.color }} />
</div>
<div>
<h3 className="font-bold text-sm" style={{ color: 'var(--text-primary)' }}>{cat.name}</h3>
<p className="text-[11px] mt-0.5 truncate" style={{ color: 'var(--text-tertiary)' }}>{cat.description || 'System Packages'}</p>
</div>
</div>
);
})}
</div>
</section>
{/* Featured Packages Section */}
<section>
<div className="flex items-center justify-between mb-4">
<div>
<p className="section-title">Recommended Packages</p>
<p className="section-subtitle">Handpicked for speed and stability</p>
</div>
<button className="btn-ghost text-xs font-semibold flex items-center gap-1.5"
style={{ color: 'var(--accent)' }}
onClick={() => navigate('/search?q=system')}>
Browse more packages <ArrowRight size={14} />
</button>
</div>
<PackageGrid packages={featured} loading={loading} />
</section>
</div>
{/* ── Right Side Panel ── */}
<div className="xl:col-span-4 flex flex-col gap-6 min-w-0">
{/* System Overview Dashboard Panel */}
<section className="card p-6 flex flex-col gap-4">
<h3 className="text-sm font-extrabold text-white flex items-center gap-2 pb-2 border-b border-[var(--border-primary)]">
<Cpu size={16} style={{ color: 'var(--accent)' }} />
Local Machine Overview
</h3>
<div className="grid grid-cols-2 gap-3">
<div className="kpi-card">
<span className="kpi-label">CPU Usage</span>
<span className="kpi-value">{sysMetrics.cpu}</span>
</div>
<div className="kpi-card">
<span className="kpi-label">Physical RAM</span>
<span className="kpi-value" style={{ fontSize: 16 }}>{sysMetrics.ram}</span>
</div>
</div>
<div className="glass-panel p-4 flex items-center gap-3">
<HardDrive size={18} style={{ color: 'var(--violet)' }} />
<div>
<span className="text-[10px] font-bold uppercase block" style={{ color: 'var(--text-tertiary)' }}>Root Drive Storage</span>
<span className="text-sm font-bold" style={{ color: 'var(--text-secondary)' }}>{sysMetrics.disk}</span>
</div>
</div>
<div className="glass-panel p-4">
<h4 className="text-xs font-bold mb-1 flex items-center gap-1.5" style={{ color: 'var(--text-primary)' }}>
<Zap size={13} style={{ color: 'var(--accent)' }} />
AUR Helper Status
</h4>
<p className="text-[11px] leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
Yay is configured as the active builder backend. System sync is synchronized with AUR APIs.
</p>
</div>
</section>
{/* Security Scanner overview banner */}
<section className="card p-6 flex flex-col gap-3 relative overflow-hidden"
style={{ background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, transparent 100%)' }}>
<h3 className="text-sm font-extrabold flex items-center gap-2 pb-2 border-b border-[var(--border-primary)]" style={{ color: 'var(--red)' }}>
<Shield size={16} /> Security Scan Engine
</h3>
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
Local static analyzer inspects PKGBUILD files for suspicious actions before running any builds.
</p>
<ul className="text-[11px] flex flex-col gap-1.5 list-disc pl-4" style={{ color: 'var(--text-tertiary)' }}>
<li>Identifies potential system mutations in install scripts</li>
<li>Flags untrusted source URLs and binary assets</li>
<li>Monitors hidden daemon integrations</li>
</ul>
</section>
{/* Upgrade Feed */}
<section className="card p-6 flex flex-col gap-3">
<h3 className="text-sm font-extrabold text-white flex items-center gap-2 pb-2 border-b border-[var(--border-primary)]">
<RefreshCw size={15} style={{ color: 'var(--amber)' }} />
Upgrade Feed
</h3>
{updateCount > 0 ? (
<div className="flex flex-col gap-2">
<div className="glass-panel p-3 flex items-start gap-2.5">
<AlertTriangle size={15} className="text-amber-500 shrink-0 mt-0.5" />
<div>
<h4 className="text-xs font-bold text-amber-500">System updates available</h4>
<p className="text-[11px] mt-1" style={{ color: 'var(--text-secondary)' }}>
There are <span className="font-bold text-white">{updateCount} packages</span> waiting to be upgraded.
</p>
<button onClick={() => navigate('/updates')} className="btn btn-primary text-[10px] py-1 px-2.5 rounded-lg mt-2 flex items-center gap-1">
Manage Updates <ArrowRight size={10} />
</button>
</div>
</div>
</div>
) : (
<div className="text-center text-xs py-4" style={{ color: 'var(--text-tertiary)' }}>
No updates available
</div>
)}
</section>
</div>
</div>
);
}
-131
View File
@@ -1,131 +0,0 @@
import { useState, useEffect } from 'react';
import api from '../api/client';
import PackageGrid from '../components/PackageGrid';
import { RefreshCw, Search, LayoutGrid, List } from 'lucide-react';
export default function Installed() {
const [packages, setPackages] = useState([]);
const [filtered, setFiltered] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
const [error, setError] = useState(null);
const [viewMode, setViewMode] = useState('grid');
useEffect(() => { load(); }, []);
useEffect(() => {
if (!filter.trim()) { setFiltered(packages); return; }
const q = filter.toLowerCase();
setFiltered(packages.filter(p =>
p.name.toLowerCase().includes(q) || (p.description && p.description.toLowerCase().includes(q))
));
}, [filter, packages]);
async function load() {
setLoading(true); setError(null);
try { const data = await api.listInstalled(); setPackages(data.results || []); }
catch (err) { setError(err.message); }
finally { setLoading(false); }
}
return (
<div className="animate-slide-up flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
<div>
<h1 className="page-title">Installed Packages</h1>
<p className="page-subtitle">
{packages.length > 0 ? `${packages.length} packages on your system` : 'Loading...'}
</p>
</div>
<div className="flex items-center gap-3">
{/* View toggle */}
<div className="flex p-1 rounded-md" style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)' }}>
<button onClick={() => setViewMode('grid')}
className="p-1.5 rounded transition-all"
style={{ background: viewMode === 'grid' ? 'var(--accent-muted)' : 'transparent', color: viewMode === 'grid' ? 'var(--accent)' : 'var(--text-tertiary)' }}>
<LayoutGrid size={13} />
</button>
<button onClick={() => setViewMode('list')}
className="p-1.5 rounded transition-all"
style={{ background: viewMode === 'list' ? 'var(--accent-muted)' : 'transparent', color: viewMode === 'list' ? 'var(--accent)' : 'var(--text-tertiary)' }}>
<List size={13} />
</button>
</div>
<button onClick={load} disabled={loading} className="btn btn-secondary text-xs">
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} /> Refresh
</button>
</div>
</div>
{error && (
<div className="rounded-lg p-3 text-xs font-medium"
style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(248,113,113,0.15)' }}>
{error}
</div>
)}
{/* Filter */}
{!loading && packages.length > 0 && (
<div className="flex flex-wrap items-center gap-3">
<div className="relative max-w-sm w-full">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2" style={{ color: 'var(--text-tertiary)' }} />
<input
type="text"
className="input pl-10 py-2 text-sm rounded-lg"
placeholder="Filter packages..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
<span className="pill">
{filtered.length} visible
</span>
</div>
)}
{/* Package List */}
{viewMode === 'grid' ? (
<PackageGrid packages={filtered} loading={loading} />
) : (
loading ? (
<div className="flex flex-col gap-2">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="card p-4 flex items-center gap-3">
<div className="shimmer h-4 w-32"></div>
<div className="shimmer h-3 w-16"></div>
<div className="flex-1"></div>
<div className="shimmer h-3 w-48"></div>
</div>
))}
</div>
) : filtered.length === 0 ? (
<p className="text-xs text-center py-8" style={{ color: 'var(--text-tertiary)' }}>No packages found</p>
) : (
<div className="data-table max-h-[520px] overflow-y-auto">
<div className="table-head">
<span>Package</span>
<span>Version</span>
<span>Source</span>
<span>Description</span>
<span>Status</span>
</div>
{filtered.map((pkg) => (
<div key={`${pkg.source}-${pkg.name}`}
className="table-row cursor-pointer"
onClick={() => window.location.href = `/package/${pkg.name}`}>
<span className="font-semibold text-slate-100 truncate">{pkg.name}</span>
<span className="font-mono text-xs text-slate-400 truncate">{pkg.version}</span>
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px] w-fit`}>
{pkg.source === 'aur' ? 'AUR' : 'pacman'}
</span>
<span className="text-xs text-slate-400 truncate">{pkg.description || '—'}</span>
<span className="text-xs font-semibold" style={{ color: 'var(--green)' }}>Installed</span>
</div>
))}
</div>
)
)}
</div>
);
}
+195
View File
@@ -0,0 +1,195 @@
import { useEffect, useState } from 'react';
import { Trash2, Info } from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { Package } from '../types';
interface InstalledProps {
onSelectPackage: (name: string) => void;
}
export default function Installed({ onSelectPackage }: InstalledProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [packages, setPackages] = useState<Package[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [filter, setFilter] = useState('');
const [sourceFilter, setSourceFilter] = useState('all');
const [removing, setRemoving] = useState<string | null>(null);
useEffect(() => {
loadPackages();
}, []);
const loadPackages = async () => {
setLoading(true);
try {
const data = await api.listInstalled();
setPackages(data.results);
} catch (err: any) {
setError(err.message || 'Failed to load installed packages');
} finally {
setLoading(false);
}
};
const handleRemove = async (name: string) => {
setRemoving(name);
try {
await api.removePackage(name);
await loadPackages();
} catch {
// silently fail
} finally {
setRemoving(null);
}
};
const filtered = packages.filter(pkg => {
const matchesName = pkg.name.toLowerCase().includes(filter.toLowerCase());
const matchesSource = sourceFilter === 'all' || pkg.source === sourceFilter;
return matchesName && matchesSource;
});
const panelClass = `border rounded-sm ${
isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'
}`;
const secondaryText = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const inputClass = `px-3 py-1.5 text-sm border rounded-sm ${
isDark
? 'bg-dark-bg border-dark-border text-dark-text'
: 'bg-light-bg border-light-border text-light-text'
}`;
const selectClass = `px-2 py-1.5 text-sm border rounded-sm ${
isDark
? 'bg-dark-bg border-dark-border text-dark-text'
: 'bg-light-bg border-light-border text-light-text'
}`;
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<div className="flex items-center justify-between">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Installed Packages
</h1>
<span className={`text-sm ${secondaryText}`}>
{packages.length} total, {filtered.length} shown
</span>
</div>
{/* Filters */}
<div className={panelClass}>
<div className="p-3 flex gap-2 items-center">
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter packages..."
className={`${inputClass} flex-1`}
/>
<select value={sourceFilter} onChange={e => setSourceFilter(e.target.value)} className={selectClass}>
<option value="all">All Sources</option>
<option value="pacman">Pacman</option>
<option value="aur">AUR</option>
</select>
<button
onClick={loadPackages}
className={`px-3 py-1.5 text-sm border rounded-sm ${
isDark
? 'border-dark-border text-dark-text-secondary hover:bg-dark-hover'
: 'border-light-border text-light-text-secondary hover:bg-light-hover'
}`}
>
Refresh
</button>
</div>
</div>
{/* Error */}
{error && (
<div className="px-3 py-2 text-sm border border-red-800 bg-red-900/20 text-red-400 rounded-sm">
{error}
</div>
)}
{/* Package list */}
<div className={panelClass}>
{loading ? (
<div className={`p-4 text-sm ${secondaryText}`}>Loading installed packages...</div>
) : filtered.length === 0 ? (
<div className={`p-4 text-sm text-center ${secondaryText}`}>
{filter ? `No packages matching "${filter}".` : 'No installed packages found.'}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="px-3 py-1.5 font-medium">Package</th>
<th className="px-3 py-1.5 font-medium">Description</th>
<th className="px-3 py-1.5 font-medium">Version</th>
<th className="px-3 py-1.5 font-medium">Source</th>
<th className="px-3 py-1.5 font-medium w-28"></th>
</tr>
</thead>
<tbody>
{filtered.map(pkg => (
<tr
key={pkg.name}
className={`border-t cursor-pointer ${
isDark
? 'border-dark-border hover:bg-dark-hover'
: 'border-light-border hover:bg-light-hover'
}`}
onClick={() => onSelectPackage(pkg.name)}
>
<td className="px-3 py-1.5 text-arch-blue font-medium whitespace-nowrap">
{pkg.name}
</td>
<td className={`px-3 py-1.5 ${secondaryText} max-w-[350px] truncate`}>
{pkg.description}
</td>
<td className={`px-3 py-1.5 text-xs font-mono ${secondaryText}`}>
{pkg.version}
</td>
<td className="px-3 py-1.5">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
pkg.source === 'aur'
? 'bg-arch-blue-light text-arch-blue'
: isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'
}`}>
{pkg.source === 'aur' ? 'AUR' : pkg.repository || 'pacman'}
</span>
</td>
<td className="px-3 py-1.5 flex gap-1">
<button
onClick={e => { e.stopPropagation(); onSelectPackage(pkg.name); }}
className={`p-1 rounded-sm ${
isDark
? 'text-dark-text-secondary hover:bg-dark-hover hover:text-dark-text'
: 'text-light-text-secondary hover:bg-light-hover hover:text-light-text'
}`}
title="Info"
>
<Info size={14} />
</button>
<button
onClick={e => { e.stopPropagation(); handleRemove(pkg.name); }}
disabled={removing === pkg.name}
className="p-1 rounded-sm text-red-400 hover:bg-red-900/20 disabled:opacity-50"
title="Remove"
>
<Trash2 size={14} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
+208
View File
@@ -0,0 +1,208 @@
import { useEffect, useState } from 'react';
import { ArrowLeft, Download, Trash2, ExternalLink, Shield } from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { Package, ScanResult } from '../types';
interface PackageDetailsProps {
packageName: string;
onBack: () => void;
}
export default function PackageDetails({ packageName, onBack }: PackageDetailsProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [pkg, setPkg] = useState<Package | null>(null);
const [scan, setScan] = useState<ScanResult | null>(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
setLoading(true);
setError('');
Promise.all([
api.getPackageInfo(packageName),
api.scanPackage(packageName).catch(() => null),
])
.then(([info, scanRes]) => {
setPkg(info);
setScan(scanRes);
})
.catch(err => setError(err.message || 'Failed to load package'))
.finally(() => setLoading(false));
}, [packageName]);
const handleInstall = async () => {
setActionLoading(true);
try {
await api.installPackage(packageName);
const info = await api.getPackageInfo(packageName);
setPkg(info);
} catch {} finally { setActionLoading(false); }
};
const handleRemove = async () => {
setActionLoading(true);
try {
await api.removePackage(packageName);
const info = await api.getPackageInfo(packageName);
setPkg(info);
} catch {} finally { setActionLoading(false); }
};
const panel = `border rounded-sm ${isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'}`;
const sec = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const row = `flex justify-between px-3 py-1.5 border-b last:border-b-0 text-sm ${isDark ? 'border-dark-border' : 'border-light-border'}`;
if (loading) return (
<div className="p-4">
<button onClick={onBack} className={`flex items-center gap-1 text-sm mb-4 ${sec} hover:text-arch-blue`}>
<ArrowLeft size={14} /> Back
</button>
<p className={`text-sm ${sec}`}>Loading package information...</p>
</div>
);
if (error || !pkg) return (
<div className="p-4">
<button onClick={onBack} className={`flex items-center gap-1 text-sm mb-4 ${sec} hover:text-arch-blue`}>
<ArrowLeft size={14} /> Back
</button>
<div className="px-3 py-2 text-sm border border-red-800 bg-red-900/20 text-red-400 rounded-sm">
{error || 'Package not found'}
</div>
</div>
);
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
{/* Header */}
<div className="flex items-center gap-3">
<button onClick={onBack} className={`flex items-center gap-1 text-sm ${sec} hover:text-arch-blue`}>
<ArrowLeft size={14} /> Back
</button>
</div>
<div className="flex items-start justify-between">
<div>
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
{pkg.name}
</h1>
<p className={`text-sm mt-1 ${sec}`}>{pkg.description}</p>
</div>
<div className="flex gap-2">
{pkg.installed ? (
<button onClick={handleRemove} disabled={actionLoading}
className="px-3 py-1.5 text-sm border border-red-700 text-red-400 rounded-sm hover:bg-red-900/20 disabled:opacity-50 flex items-center gap-1.5">
<Trash2 size={14} /> {actionLoading ? 'Removing...' : 'Remove'}
</button>
) : (
<button onClick={handleInstall} disabled={actionLoading}
className="px-3 py-1.5 text-sm bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover disabled:opacity-50 flex items-center gap-1.5">
<Download size={14} /> {actionLoading ? 'Installing...' : 'Install'}
</button>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Package Info */}
<div className={panel}>
<div className={`px-3 py-2 border-b font-medium text-sm ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
Package Information
</div>
<div className={row}><span className={sec}>Version</span><span className="font-mono text-xs">{pkg.version}</span></div>
<div className={row}><span className={sec}>Source</span>
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${pkg.source === 'aur' ? 'bg-arch-blue-light text-arch-blue' : isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'}`}>
{pkg.source === 'aur' ? 'AUR' : pkg.repository || 'pacman'}
</span>
</div>
<div className={row}><span className={sec}>Status</span>
{pkg.installed ? <span className="text-green-500 text-xs">Installed</span> : <span className={`text-xs ${sec}`}>Not installed</span>}
</div>
{pkg.installed_version && <div className={row}><span className={sec}>Installed Version</span><span className="font-mono text-xs">{pkg.installed_version}</span></div>}
{pkg.arch && <div className={row}><span className={sec}>Architecture</span><span className="text-xs">{pkg.arch}</span></div>}
{pkg.licenses && pkg.licenses.length > 0 && <div className={row}><span className={sec}>License</span><span className="text-xs">{pkg.licenses.join(', ')}</span></div>}
{pkg.size && <div className={row}><span className={sec}>Download Size</span><span className="text-xs">{pkg.size}</span></div>}
{pkg.install_size && <div className={row}><span className={sec}>Installed Size</span><span className="text-xs">{pkg.install_size}</span></div>}
{pkg.packager && <div className={row}><span className={sec}>Packager</span><span className="text-xs truncate max-w-[200px]">{pkg.packager}</span></div>}
{pkg.build_date && <div className={row}><span className={sec}>Build Date</span><span className="text-xs">{pkg.build_date}</span></div>}
{pkg.url && (
<div className={row}><span className={sec}>URL</span>
<a href={pkg.url} target="_blank" rel="noreferrer" className="text-xs text-arch-blue hover:underline flex items-center gap-1">
{pkg.url.replace(/^https?:\/\//, '').slice(0, 40)} <ExternalLink size={10} />
</a>
</div>
)}
</div>
{/* Right column */}
<div className="space-y-4">
{/* Dependencies */}
{pkg.depends && pkg.depends.length > 0 && (
<div className={panel}>
<div className={`px-3 py-2 border-b font-medium text-sm ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
Dependencies ({pkg.depends.length})
</div>
<div className="p-3 flex flex-wrap gap-1">
{pkg.depends.map(dep => (
<span key={dep} className={`text-xs px-1.5 py-0.5 rounded-sm ${isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'}`}>
{dep}
</span>
))}
</div>
</div>
)}
{/* Optional deps */}
{pkg.opt_depends && pkg.opt_depends.length > 0 && (
<div className={panel}>
<div className={`px-3 py-2 border-b font-medium text-sm ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
Optional Dependencies ({pkg.opt_depends.length})
</div>
<div className="p-3 flex flex-wrap gap-1">
{pkg.opt_depends.map(dep => (
<span key={dep} className={`text-xs px-1.5 py-0.5 rounded-sm ${isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'}`}>
{dep}
</span>
))}
</div>
</div>
)}
{/* Security scan */}
{scan && scan.scanned && (
<div className={panel}>
<div className={`px-3 py-2 border-b font-medium text-sm flex items-center gap-1.5 ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<Shield size={14} className="text-arch-blue" /> Security Scan
</div>
<div className={row}>
<span className={sec}>Risk Level</span>
<span className={`text-xs px-1.5 py-0.5 rounded-sm font-medium ${
scan.risk_level === 'low' ? 'bg-green-900/30 text-green-400'
: scan.risk_level === 'medium' ? 'bg-yellow-900/30 text-yellow-400'
: 'bg-red-900/30 text-red-400'
}`}>{scan.risk_level}</span>
</div>
<div className={row}><span className={sec}>Risk Score</span><span className="text-xs">{scan.risk_score}/100</span></div>
{scan.findings.length > 0 && (
<div className="p-3">
<p className={`text-xs font-medium mb-1 ${sec}`}>Findings:</p>
{scan.findings.map((f, i) => (
<div key={i} className={`text-xs py-1 border-t ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<span className={`font-medium ${f.severity === 'high' ? 'text-red-400' : f.severity === 'medium' ? 'text-yellow-400' : 'text-green-400'}`}>
[{f.severity}]
</span>{' '}
{f.description}
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
-282
View File
@@ -1,282 +0,0 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import api from '../api/client';
import LoadingSpinner from '../components/LoadingSpinner';
import {
ArrowLeft, Globe, ExternalLink, Download, Trash2,
ShieldCheck, ShieldAlert, Shield, User, Clock, Package, AlertTriangle
} from 'lucide-react';
export default function PackageView() {
const { packageName } = useParams();
const navigate = useNavigate();
const [pkg, setPkg] = useState(null);
const [scan, setScan] = useState(null);
const [loading, setLoading] = useState(true);
const [scanning, setScanning] = useState(false);
const [acting, setActing] = useState(false);
const [log, setLog] = useState('');
const [error, setError] = useState(null);
useEffect(() => { load(); }, [packageName]);
async function load() {
setLoading(true); setError(null);
try {
const d = await api.getPackageInfo(packageName);
setPkg(d);
if (d.source === 'aur') doScan(d.name);
} catch (e) { setError(e.message); }
finally { setLoading(false); }
}
async function doScan(name) {
setScanning(true);
try { setScan(await api.scanPackage(name)); }
catch { /* silent */ }
finally { setScanning(false); }
}
async function install() {
setActing(true); setLog('Installing...\n'); setError(null);
try {
const r = await api.installPackage(pkg.name);
if (r.success) { setLog(p => p + '\n✓ Installed!\n' + r.message); setPkg(await api.getPackageInfo(pkg.name)); }
else setError('Installation failed: ' + r.message);
} catch (e) { setError(e.message); }
finally { setActing(false); }
}
async function remove() {
if (!confirm(`Remove ${pkg.name}?`)) return;
setActing(true); setLog('Removing...\n'); setError(null);
try {
const r = await api.removePackage(pkg.name);
if (r.success) { setLog(p => p + '\n✓ Removed!\n' + r.message); setPkg(await api.getPackageInfo(pkg.name)); }
else setError('Removal failed: ' + r.message);
} catch (e) { setError(e.message); }
finally { setActing(false); }
}
if (loading) return <LoadingSpinner text="Loading package details..." />;
if (error && !pkg) {
return (
<div className="animate-slide-up max-w-2xl">
<button onClick={() => navigate(-1)} className="btn btn-secondary mb-4"><ArrowLeft size={13} /> Back</button>
<div className="rounded-lg p-4 text-xs" style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(248,113,113,0.15)' }}>
<p className="font-semibold mb-0.5">Package not found</p>
<p>{error}</p>
</div>
</div>
);
}
const riskColor = !scan?.scanned ? 'var(--text-tertiary)'
: scan.risk_score >= 70 ? 'var(--red)'
: scan.risk_score >= 40 ? 'var(--amber)'
: 'var(--green)';
return (
<div className="animate-slide-up flex flex-col gap-6">
<div>
<button onClick={() => navigate(-1)} className="btn btn-secondary mb-3"><ArrowLeft size={13} /> Back</button>
</div>
{error && (
<div className="rounded-lg p-3 text-xs" style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(248,113,113,0.15)' }}>
{error}
</div>
)}
{acting && (
<div className="card p-4" style={{ borderColor: 'var(--border-glow)' }}>
<div className="flex items-center gap-2 mb-2 text-xs font-semibold" style={{ color: 'var(--accent)' }}>
<div className="spinner" style={{ width: 12, height: 12, borderWidth: 1.5 }}></div>
Working...
</div>
<pre className="p-3 rounded font-mono text-[10px] max-h-36 overflow-auto whitespace-pre-wrap"
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)', color: 'var(--text-secondary)' }}>
{log}
</pre>
</div>
)}
{/* Main Info Card */}
<div className="card p-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
<div className="lg:col-span-8 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-3">
<h1 className="text-2xl font-bold leading-tight" style={{ letterSpacing: '-0.02em', color: 'var(--text-primary)' }}>
{pkg.name}
</h1>
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'}`}>
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
</span>
{pkg.installed && <span className="badge badge-installed">Installed</span>}
{pkg.out_of_date && (
<span className="badge" style={{ background: 'var(--amber-muted)', color: 'var(--amber)' }}>
<AlertTriangle size={10} className="mr-1" /> Out of Date
</span>
)}
</div>
<p className="text-sm leading-relaxed mb-4" style={{ color: 'var(--text-secondary)' }}>
{pkg.description || 'No description available.'}
</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<MetaBox label="Version" value={pkg.version} mono />
{pkg.source === 'aur' ? (
<>
<MetaBox label="Votes" value={pkg.votes} />
<MetaBox label="Popularity" value={pkg.popularity?.toFixed(2) || '0.00'} />
<MetaBox label="Status" value={pkg.out_of_date ? 'Out of Date' : 'Current'}
color={pkg.out_of_date ? 'var(--amber)' : 'var(--green)'} />
</>
) : (
<>
<MetaBox label="Repository" value={pkg.repository || 'pacman'} />
<MetaBox label="State" value={pkg.installed ? 'Installed' : 'Available'} span={2} />
</>
)}
</div>
</div>
{/* Action column */}
<div className="lg:col-span-4 flex flex-col gap-2 justify-center">
{pkg.installed ? (
<button onClick={remove} disabled={acting} className="btn btn-danger w-full py-2">
<Trash2 size={13} /> Uninstall
</button>
) : (
<button onClick={install} disabled={acting} className="btn btn-primary w-full py-2">
<Download size={13} /> Install
</button>
)}
{pkg.url && (
<a href={pkg.url} target="_blank" rel="noopener noreferrer"
className="btn btn-secondary w-full py-2 no-underline text-center justify-center flex items-center gap-1.5">
<Globe size={13} /> Website <ExternalLink size={10} />
</a>
)}
</div>
</div>
</div>
{/* Security Analysis */}
{pkg.source === 'aur' && (
<div className="card p-4">
<h3 className="font-bold text-xs flex items-center gap-1.5 mb-3 pb-2"
style={{ borderBottom: '1px solid var(--border-primary)', color: 'var(--text-primary)' }}>
<Shield size={14} style={{ color: 'var(--accent)' }} />
Security Scan (AUR PKGBUILD)
</h3>
{scanning ? (
<LoadingSpinner size="sm" text="Scanning PKGBUILD..." />
) : scan?.scanned ? (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between p-3 rounded-lg"
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)' }}>
<div className="flex items-center gap-2">
<span className="font-semibold text-xs capitalize" style={{ color: riskColor }}>
{scan.risk_level} Risk
</span>
</div>
<div className="text-right">
<span className="text-lg font-bold" style={{ color: riskColor }}>{scan.risk_score}</span>
<span className="text-[10px]" style={{ color: 'var(--text-tertiary)' }}>/100</span>
</div>
</div>
{scan.findings.filter(f => f.severity !== 'info').length > 0 ? (
<div className="flex flex-col gap-1.5">
{scan.findings.filter(f => f.severity !== 'info').map((f, i) => (
<div key={i} className="p-2.5 rounded-lg text-[11px] flex flex-col gap-1"
style={{
background: f.severity === 'critical' ? 'var(--red-muted)' : 'var(--amber-muted)',
border: `1px solid ${f.severity === 'critical' ? 'rgba(248,113,113,0.15)' : 'rgba(251,191,36,0.15)'}`,
color: f.severity === 'critical' ? 'var(--red)' : 'var(--amber)',
}}>
<div className="flex justify-between font-bold uppercase text-[9px] tracking-wide">
<span>{f.severity}</span>
{f.line_number > 0 && <span className="opacity-70">Line {f.line_number}</span>}
</div>
<p style={{ color: 'var(--text-primary)' }}>{f.description}</p>
{f.line_content && (
<pre className="p-1.5 rounded font-mono text-[9px] mt-1 overflow-x-auto"
style={{ background: 'var(--bg-primary)', color: 'var(--text-tertiary)' }}>
{f.line_content}
</pre>
)}
</div>
))}
</div>
) : (
<div className="p-3 rounded-lg text-xs text-center font-medium"
style={{ background: 'var(--green-muted)', color: 'var(--green)', border: '1px solid rgba(52,211,153,0.15)' }}>
No security issues found in static PKGBUILD check.
</div>
)}
</div>
) : (
<p className="text-[11px] text-center py-2" style={{ color: 'var(--text-tertiary)' }}>
Scan not available.
</p>
)}
</div>
)}
{/* Metadata Detail Row Panels */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="card p-4">
<h3 className="font-bold text-xs mb-3 flex items-center gap-1.5 pb-2"
style={{ borderBottom: '1px solid var(--border-primary)', color: 'var(--text-primary)' }}>
<User size={13} /> Package Metadata
</h3>
<div className="flex flex-col gap-2 text-[11px]">
{pkg.maintainer && <MetaRow label="Maintainer" value={pkg.maintainer} />}
{pkg.last_modified > 0 && <MetaRow label="Last Modified" value={new Date(pkg.last_modified * 1000).toLocaleDateString()} />}
{pkg.first_submitted > 0 && <MetaRow label="First Submitted" value={new Date(pkg.first_submitted * 1000).toLocaleDateString()} />}
{pkg.package_base && <MetaRow label="Package Base" value={pkg.package_base} />}
</div>
</div>
<div className="card p-4">
<h3 className="font-bold text-xs mb-3 flex items-center gap-1.5 pb-2"
style={{ borderBottom: '1px solid var(--border-primary)', color: 'var(--text-primary)' }}>
<Package size={13} /> Dependency Handling
</h3>
<div className="p-3 rounded-lg text-[11px] flex items-start gap-2"
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)' }}>
<Clock size={13} style={{ color: 'var(--accent)', flexShrink: 0, marginTop: 1 }} />
<span style={{ color: 'var(--text-secondary)', lineHeight: 1.4 }}>
Dependencies are automatically analyzed, resolved, and processed by standard package management operations during final deployment.
</span>
</div>
</div>
</div>
</div>
);
}
function MetaBox({ label, value, mono, color, span }) {
return (
<div className={`p-2 rounded-lg ${span === 2 ? 'col-span-2' : ''}`}
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)' }}>
<p className="text-[9px] uppercase font-bold tracking-wide mb-0.5" style={{ color: 'var(--text-tertiary)' }}>{label}</p>
<p className={`text-xs font-semibold truncate ${mono ? 'font-mono' : ''}`}
style={{ color: color || 'var(--text-primary)' }}>{value || '—'}</p>
</div>
);
}
function MetaRow({ label, value }) {
return (
<div className="flex justify-between items-center py-0.5">
<span style={{ color: 'var(--text-tertiary)' }}>{label}</span>
<span className="font-semibold text-right" style={{ color: 'var(--text-primary)' }}>{value}</span>
</div>
);
}
-483
View File
@@ -1,483 +0,0 @@
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import api from '../api/client';
import PackageGrid from '../components/PackageGrid';
import LoadingSpinner from '../components/LoadingSpinner';
import {
Filter, Search, Grid, List, CheckCircle, AlertTriangle, Star,
Download, Globe, ExternalLink, Trash2, Shield, Package
} from 'lucide-react';
export default function SearchPage() {
const [searchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [source, setSource] = useState('all');
const [error, setError] = useState(null);
const [layoutMode, setLayoutMode] = useState('grid');
const [sortBy, setSortBy] = useState('relevance');
const [filters, setFilters] = useState({ installed: false, outOfDate: false, hasVotes: false });
// Selected package details for right preview panel
const [selectedPkgName, setSelectedPkgName] = useState(null);
const [pkgDetail, setPkgDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [scanResult, setScanResult] = useState(null);
const [scanning, setScanning] = useState(false);
const [actionLog, setActionLog] = useState('');
const [acting, setActing] = useState(false);
useEffect(() => {
if (query.trim()) {
performSearch(query, source);
} else {
setResults([]);
setSelectedPkgName(null);
setPkgDetail(null);
}
}, [query, source]);
useEffect(() => {
if (selectedPkgName) {
loadDetails(selectedPkgName);
} else {
setPkgDetail(null);
setScanResult(null);
}
}, [selectedPkgName]);
async function performSearch(q, s) {
setLoading(true);
setError(null);
try {
const data = await api.searchPackages(q, s);
const resList = data.results || [];
setResults(resList);
if (resList.length > 0) {
setSelectedPkgName(resList[0].name);
} else {
setSelectedPkgName(null);
}
} catch (err) {
setError(err.message);
setResults([]);
setSelectedPkgName(null);
} finally {
setLoading(false);
}
}
async function loadDetails(name) {
setDetailLoading(true);
setScanResult(null);
setActionLog('');
try {
const detail = await api.getPackageInfo(name);
setPkgDetail(detail);
if (detail.source === 'aur') {
doScan(detail.name);
}
} catch {
// silent fallback
} finally {
setDetailLoading(false);
}
}
async function doScan(name) {
setScanning(true);
try {
const scan = await api.scanPackage(name);
setScanResult(scan);
} catch {
// silent fallback
} finally {
setScanning(false);
}
}
async function handleInstall() {
if (!pkgDetail) return;
setActing(true);
setActionLog('Retrieving database structures...\nInitializing builder pipeline...\n');
try {
const res = await api.installPackage(pkgDetail.name);
if (res.success) {
setActionLog(prev => prev + '\n✓ Build completed successfully!\n' + res.message);
loadDetails(pkgDetail.name);
} else {
setError('Installation failed: ' + res.message);
}
} catch (e) {
setError(e.message);
} finally {
setActing(false);
}
}
async function handleUninstall() {
if (!pkgDetail || !confirm(`Remove ${pkgDetail.name}?`)) return;
setActing(true);
setActionLog('Resolving package graph...\nRemoving target dependencies...\n');
try {
const res = await api.removePackage(pkgDetail.name);
if (res.success) {
setActionLog(prev => prev + '\n✓ Uninstallation complete!\n' + res.message);
loadDetails(pkgDetail.name);
} else {
setError('Removal failed: ' + res.message);
}
} catch (e) {
setError(e.message);
} finally {
setActing(false);
}
}
const tabs = [
{ id: 'all', label: 'All Packages' },
{ id: 'pacman', label: 'Official' },
{ id: 'aur', label: 'AUR Repos' },
];
const filteredResults = results
.filter((pkg) => !filters.installed || pkg.installed)
.filter((pkg) => !filters.outOfDate || pkg.out_of_date)
.filter((pkg) => !filters.hasVotes || (pkg.votes || 0) > 0);
const sortedResults = [...filteredResults].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'votes') return (b.votes || 0) - (a.votes || 0);
if (sortBy === 'popularity') return (b.popularity || 0) - (a.popularity || 0);
return 0;
});
return (
<div className="animate-slide-up flex flex-col gap-6 w-full">
{/* Search page sub-header */}
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
<div>
<h1 className="page-title text-white">Advanced Search</h1>
<p className="page-subtitle">
{query ? <>Queried results for <strong style={{ color: 'var(--accent)' }}>&quot;{query}&quot;</strong></> : 'Query pacman databases and AUR repositories'}
</p>
</div>
{query && (
<div className="flex flex-wrap items-center gap-3">
<div className="flex p-1 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-primary)]">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setSource(tab.id)}
className="px-4 py-1.5 text-xs font-semibold rounded-md transition-all cursor-pointer"
style={{
background: source === tab.id ? 'var(--accent)' : 'transparent',
color: source === tab.id ? '#fff' : 'var(--text-secondary)',
}}
>
{tab.label}
</button>
))}
</div>
<span className="flex items-center gap-1.5 text-xs font-semibold" style={{ color: 'var(--text-tertiary)' }}>
<Filter size={13} /> {sortedResults.length} results
</span>
</div>
)}
</div>
{error && (
<div className="rounded-xl p-4 text-xs font-semibold"
style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
{error}
</div>
)}
{query ? (
/* Triple Column Split Layout */
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 items-start w-full">
{/* Results Panel */}
<div className="xl:col-span-8 flex flex-col gap-4 min-w-0">
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
<input type="checkbox" checked={filters.installed} onChange={() => setFilters((prev) => ({ ...prev, installed: !prev.installed }))} />
Installed
</label>
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
<input type="checkbox" checked={filters.outOfDate} onChange={() => setFilters((prev) => ({ ...prev, outOfDate: !prev.outOfDate }))} />
Out of date
</label>
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
<input type="checkbox" checked={filters.hasVotes} onChange={() => setFilters((prev) => ({ ...prev, hasVotes: !prev.hasVotes }))} />
Has votes
</label>
</div>
<div className="flex items-center gap-2 ml-auto">
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>Sort</span>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="input !py-1.5 !px-2 text-xs"
style={{ width: 140 }}
>
{['relevance', 'name', 'votes', 'popularity'].map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="pill">
<CheckCircle size={12} /> {results.filter(r => r.installed).length} installed
</span>
<span className="pill">
<AlertTriangle size={12} /> {results.filter(r => r.out_of_date).length} out of date
</span>
</div>
<div className="flex items-center gap-2">
<button
className="btn-ghost !p-2 rounded-lg"
style={{ color: layoutMode === 'grid' ? 'var(--accent)' : 'var(--text-tertiary)' }}
onClick={() => setLayoutMode('grid')}
>
<Grid size={15} />
</button>
<button
className="btn-ghost !p-2 rounded-lg"
style={{ color: layoutMode === 'list' ? 'var(--accent)' : 'var(--text-tertiary)' }}
onClick={() => setLayoutMode('list')}
>
<List size={15} />
</button>
</div>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card p-4 flex flex-col gap-2">
<div className="shimmer h-4 w-32"></div>
<div className="shimmer h-3 w-16 mb-2"></div>
<div className="shimmer h-3 w-full"></div>
<div className="shimmer h-3 w-3/4"></div>
</div>
))}
</div>
) : sortedResults.length === 0 ? (
<div className="card p-12 text-center flex flex-col items-center justify-center gap-3">
<Package size={36} className="text-slate-500" />
<h3 className="font-bold text-base text-slate-200">No packages match the query</h3>
<p className="text-xs text-slate-400">Refine the query or check for alternative repository scopes.</p>
</div>
) : layoutMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sortedResults.map((pkg) => (
<div
key={`${pkg.source}-${pkg.name}`}
className={`card card-interactive p-4 flex flex-col justify-between min-h-[150px] ${selectedPkgName === pkg.name ? 'border-[var(--border-secondary)]' : ''}`}
onClick={() => setSelectedPkgName(pkg.name)}
>
<div>
<div className="flex items-center justify-between gap-2 mb-2">
<span className="font-extrabold text-sm text-slate-100 truncate flex items-center gap-1.5">
{pkg.name}
{pkg.installed && <CheckCircle size={12} className="text-[var(--green)]" />}
{pkg.out_of_date && <AlertTriangle size={12} className="text-[var(--amber)]" />}
</span>
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px]`}>
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
</span>
</div>
<p className="text-xs text-slate-300 line-clamp-2 min-h-[2.6em] leading-relaxed">
{pkg.description || 'No description available.'}
</p>
</div>
<div className="flex items-center justify-between pt-3 mt-3 border-t border-[var(--border-primary)] text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
<span className="font-mono text-slate-400 font-semibold">{pkg.version || '—'}</span>
{pkg.installed ? (
<span className="text-[var(--green)] font-bold">Installed</span>
) : (
<span className="text-[var(--accent)] font-semibold flex items-center gap-1">
<Download size={11} /> Preview
</span>
)}
</div>
</div>
))}
</div>
) : (
<div className="data-table">
<div className="table-head">
<span>Name</span>
<span>Version</span>
<span>Source</span>
<span>Description</span>
<span>Status</span>
</div>
{sortedResults.map((pkg) => (
<div
key={`${pkg.source}-${pkg.name}`}
className={`table-row ${selectedPkgName === pkg.name ? 'ring-1 ring-[var(--accent-glow)]' : ''}`}
onClick={() => setSelectedPkgName(pkg.name)}
>
<span className="font-semibold text-slate-100 truncate">{pkg.name}</span>
<span className="font-mono text-xs text-slate-400 truncate">{pkg.version || '—'}</span>
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px] w-fit`}>
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
</span>
<span className="text-xs text-slate-400 truncate">{pkg.description || 'No description available.'}</span>
<span className="text-xs font-semibold" style={{ color: pkg.installed ? 'var(--green)' : 'var(--text-tertiary)' }}>
{pkg.installed ? 'Installed' : 'Available'}
</span>
</div>
))}
</div>
)}
</div>
{/* Right Panel: detailed specification drawer */}
<div className="xl:col-span-4 card p-5 flex flex-col gap-5 sticky top-24 max-h-[calc(100vh-130px)] overflow-y-auto min-w-0">
{detailLoading ? (
<LoadingSpinner text="Fetching full package schema..." />
) : pkgDetail ? (
<div className="flex flex-col gap-4">
{/* Header info */}
<div>
<div className="flex flex-wrap items-center gap-2 mb-2">
<span className="text-base font-extrabold text-white">{pkgDetail.name}</span>
<span className={`badge ${pkgDetail.source === 'aur' ? 'badge-aur' : 'badge-pacman'}`}>
{pkgDetail.source === 'aur' ? 'AUR' : 'pacman'}
</span>
{pkgDetail.installed && <span className="badge badge-installed">Active</span>}
</div>
<p className="text-xs text-slate-300 leading-relaxed">
{pkgDetail.description || 'No description specs available for this system library.'}
</p>
</div>
{/* Console build output */}
{acting && (
<div className="bg-slate-950 border border-[var(--border-glow)] rounded-xl p-3">
<span className="text-[10px] font-bold text-slate-400 block mb-1">Process output console</span>
<pre className="font-mono text-[9px] text-[var(--accent)] whitespace-pre-wrap max-h-32 overflow-y-auto leading-normal">
{actionLog}
</pre>
</div>
)}
{/* Actions Row */}
<div className="grid grid-cols-2 gap-3 pb-3 border-b border-[var(--border-primary)]">
{pkgDetail.installed ? (
<button onClick={handleUninstall} disabled={acting} className="btn btn-danger py-2">
<Trash2 size={13} /> Remove pkg
</button>
) : (
<button onClick={handleInstall} disabled={acting} className="btn btn-primary py-2">
<Download size={13} /> Install package
</button>
)}
{pkgDetail.url ? (
<a href={pkgDetail.url} target="_blank" rel="noopener noreferrer"
className="btn btn-secondary py-2 no-underline text-center justify-center flex items-center gap-1.5">
<Globe size={13} /> Website <ExternalLink size={10} />
</a>
) : (
<button className="btn btn-secondary opacity-50 cursor-not-allowed">No specs URL</button>
)}
</div>
{/* Static scan widget (AUR only) */}
{pkgDetail.source === 'aur' && (
<div className="bg-slate-900/40 border border-[var(--border-primary)] rounded-xl p-3.5 flex flex-col gap-2.5">
<h4 className="text-xs font-bold text-slate-100 flex items-center gap-1.5">
<Shield size={13} style={{ color: 'var(--accent)' }} /> PKGBUILD Security Scan
</h4>
{scanning ? (
<span className="text-[10px] text-slate-400 animate-pulse">Running static scan routines...</span>
) : scanResult ? (
<div className="flex flex-col gap-2 text-[11px]">
<div className="flex justify-between items-center bg-slate-950 p-2 rounded border border-[var(--border-primary)]">
<span className="font-bold text-slate-300">Risk Assessment:</span>
<span className="font-extrabold"
style={{
color: scanResult.risk_score >= 70 ? 'var(--red)'
: scanResult.risk_score >= 40 ? 'var(--amber)'
: 'var(--green)'
}}>
{scanResult.risk_score} / 100 ({scanResult.risk_level})
</span>
</div>
{scanResult.findings.length > 0 ? (
<div className="text-[10px] text-red-400 bg-red-950/20 border border-red-500/10 p-2 rounded">
Static analyzer detected script mutations or potentially unsafe downloads. Verify manual details in PKGBUILD.
</div>
) : (
<div className="text-[10px] text-green-400 bg-green-950/20 border border-green-500/10 p-2 rounded">
Verified PKGBUILD check completed. No suspicious mutations found.
</div>
)}
</div>
) : (
<span className="text-[10px] text-slate-400">Scan details not loaded.</span>
)}
</div>
)}
{/* Grid Metadata details */}
<div className="flex flex-col gap-2 text-[11px] bg-slate-950/50 p-3.5 rounded-xl border border-[var(--border-primary)]">
<div className="flex justify-between py-1 border-b border-[var(--border-primary)]/40">
<span style={{ color: 'var(--text-tertiary)' }}>Package version</span>
<span className="font-mono text-slate-200">{pkgDetail.version}</span>
</div>
{pkgDetail.votes !== undefined && (
<div className="flex justify-between py-1 border-b border-[var(--border-primary)]/40">
<span style={{ color: 'var(--text-tertiary)' }}>AUR Vote Weight</span>
<span className="font-semibold text-slate-200 flex items-center gap-0.5">
<Star size={11} className="text-amber-500 fill-amber-500" /> {pkgDetail.votes}
</span>
</div>
)}
{pkgDetail.maintainer && (
<div className="flex justify-between py-1">
<span style={{ color: 'var(--text-tertiary)' }}>AUR Maintainer</span>
<span className="font-bold text-slate-200">{pkgDetail.maintainer}</span>
</div>
)}
</div>
</div>
) : (
<div className="text-center py-20 text-slate-400 flex flex-col items-center gap-3">
<Shield size={32} className="text-slate-500" />
<p className="text-xs font-semibold text-slate-300">Select a Package for Details</p>
<p className="text-[10px] max-w-[200px] leading-relaxed">
Click on any searched package to review security scans, versions, votes, and maintainer specifications instantly.
</p>
</div>
)}
</div>
</div>
) : (
/* Empty State Illustration */
<div className="card p-16 text-center flex flex-col items-center justify-center gap-4 max-w-2xl mx-auto mt-8">
<div className="w-14 h-14 rounded-2xl flex items-center justify-center bg-slate-900 border border-[var(--border-glow)]"
style={{ boxShadow: 'var(--shadow-glow)' }}>
<Search size={28} className="text-[var(--accent)]" />
</div>
<h2 className="text-lg font-extrabold text-white">Find Arch Software Instantly</h2>
<p className="text-xs text-slate-400 max-w-sm leading-relaxed">
Search across official core, extra, multilib repositories, and the community-driven AUR. Use the top navigation bar to initialize queries.
</p>
</div>
)}
</div>
);
}
+230
View File
@@ -0,0 +1,230 @@
import { useState } from 'react';
import { Search as SearchIcon, Download } from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { Package } from '../types';
interface SearchProps {
onSelectPackage: (name: string) => void;
}
export default function SearchPage({ onSelectPackage }: SearchProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [query, setQuery] = useState('');
const [source, setSource] = useState('all');
const [sort, setSort] = useState('name');
const [results, setResults] = useState<Package[]>([]);
const [loading, setLoading] = useState(false);
const [searched, setSearched] = useState(false);
const [error, setError] = useState('');
const [installing, setInstalling] = useState<string | null>(null);
const doSearch = async () => {
if (!query.trim()) return;
setLoading(true);
setError('');
setSearched(true);
try {
const data = await api.searchPackages(query.trim(), source);
let sorted = [...data.results];
if (sort === 'name') sorted.sort((a, b) => a.name.localeCompare(b.name));
else if (sort === 'repo') sorted.sort((a, b) => a.repository.localeCompare(b.repository));
setResults(sorted);
} catch (err: any) {
setError(err.message || 'Search failed');
setResults([]);
} finally {
setLoading(false);
}
};
const handleInstall = async (name: string) => {
setInstalling(name);
try {
await api.installPackage(name);
// Refresh search
await doSearch();
} catch {
// silently fail for demo
} finally {
setInstalling(null);
}
};
const panelClass = `border rounded-sm ${
isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'
}`;
const secondaryText = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const inputClass = `px-3 py-1.5 text-sm border rounded-sm ${
isDark
? 'bg-dark-bg border-dark-border text-dark-text'
: 'bg-light-bg border-light-border text-light-text'
}`;
const selectClass = `px-2 py-1.5 text-sm border rounded-sm ${
isDark
? 'bg-dark-bg border-dark-border text-dark-text'
: 'bg-light-bg border-light-border text-light-text'
}`;
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Search Packages
</h1>
{/* Search controls */}
<div className={panelClass}>
<div className={`px-3 py-2 border-b ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<span className={`text-sm font-medium ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Search Options
</span>
</div>
<div className="p-3 flex gap-2 items-end flex-wrap">
<div className="flex-1 min-w-[250px]">
<label className={`block text-xs mb-1 ${secondaryText}`}>Package Name</label>
<div className="relative">
<SearchIcon size={14} className={`absolute left-2.5 top-1/2 -translate-y-1/2 ${secondaryText}`} />
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && doSearch()}
placeholder="Enter package name..."
className={`${inputClass} w-full pl-8`}
/>
</div>
</div>
<div>
<label className={`block text-xs mb-1 ${secondaryText}`}>Repository</label>
<select value={source} onChange={e => setSource(e.target.value)} className={selectClass}>
<option value="all">All Sources</option>
<option value="pacman">Pacman Only</option>
<option value="aur">AUR Only</option>
</select>
</div>
<div>
<label className={`block text-xs mb-1 ${secondaryText}`}>Sort By</label>
<select value={sort} onChange={e => setSort(e.target.value)} className={selectClass}>
<option value="name">Name</option>
<option value="repo">Repository</option>
</select>
</div>
<button
onClick={doSearch}
disabled={loading || !query.trim()}
className="px-4 py-1.5 text-sm bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover disabled:opacity-50"
>
{loading ? 'Searching...' : 'Search'}
</button>
</div>
</div>
{/* Results */}
{error && (
<div className="px-3 py-2 text-sm border border-red-800 bg-red-900/20 text-red-400 rounded-sm">
{error}
</div>
)}
{searched && (
<div className={panelClass}>
<div className={`px-3 py-2 border-b flex items-center justify-between ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<span className={`text-sm font-medium ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
Results
</span>
<span className={`text-xs ${secondaryText}`}>
{results.length} package{results.length !== 1 ? 's' : ''} found
</span>
</div>
{results.length === 0 && !loading ? (
<div className={`p-4 text-sm text-center ${secondaryText}`}>
No packages found matching "{query}".
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="px-3 py-1.5 font-medium">Package</th>
<th className="px-3 py-1.5 font-medium">Description</th>
<th className="px-3 py-1.5 font-medium">Repository</th>
<th className="px-3 py-1.5 font-medium">Version</th>
<th className="px-3 py-1.5 font-medium">Status</th>
<th className="px-3 py-1.5 font-medium w-24"></th>
</tr>
</thead>
<tbody>
{results.map(pkg => (
<tr
key={`${pkg.name}-${pkg.source}`}
className={`border-t cursor-pointer ${
isDark
? 'border-dark-border hover:bg-dark-hover'
: 'border-light-border hover:bg-light-hover'
}`}
onClick={() => onSelectPackage(pkg.name)}
>
<td className="px-3 py-1.5 text-arch-blue font-medium whitespace-nowrap">
{pkg.name}
</td>
<td className={`px-3 py-1.5 ${secondaryText} max-w-[300px] truncate`}>
{pkg.description}
</td>
<td className="px-3 py-1.5">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
pkg.source === 'aur'
? 'bg-arch-blue-light text-arch-blue'
: isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'
}`}>
{pkg.source === 'aur' ? 'AUR' : pkg.repository || 'pacman'}
</span>
</td>
<td className={`px-3 py-1.5 text-xs font-mono ${secondaryText}`}>
{pkg.version}
</td>
<td className="px-3 py-1.5">
{pkg.installed ? (
<span className="text-xs text-green-500">Installed</span>
) : (
<span className={`text-xs ${secondaryText}`}>Not installed</span>
)}
</td>
<td className="px-3 py-1.5">
{pkg.installed ? (
<button
onClick={e => { e.stopPropagation(); onSelectPackage(pkg.name); }}
className={`px-2 py-0.5 text-xs border rounded-sm ${
isDark
? 'border-dark-border text-dark-text-secondary hover:bg-dark-hover'
: 'border-light-border text-light-text-secondary hover:bg-light-hover'
}`}
>
Info
</button>
) : (
<button
onClick={e => { e.stopPropagation(); handleInstall(pkg.name); }}
disabled={installing === pkg.name}
className="px-2 py-0.5 text-xs bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover disabled:opacity-50 flex items-center gap-1"
>
<Download size={11} />
{installing === pkg.name ? '...' : 'Install'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
}
-191
View File
@@ -1,191 +0,0 @@
import { useState, useEffect } from 'react';
import api from '../api/client';
import { Database, Cpu, Heart, CheckCircle2, AlertCircle, Bell, ShieldCheck, Palette } from 'lucide-react';
export default function Settings() {
const [clearing, setClearing] = useState(false);
const [health, setHealth] = useState(null);
const [checking, setChecking] = useState(false);
const [msg, setMsg] = useState(null);
const [toggles, setToggles] = useState({
autoUpdates: true,
notifications: true,
securityScan: true,
compactMode: false,
});
useEffect(() => { checkHealth(); }, []);
async function checkHealth() {
setChecking(true);
try { setHealth(await api.healthCheck()); }
catch (e) { setHealth({ status: 'offline', error: e.message }); }
finally { setChecking(false); }
}
async function clearCache() {
setClearing(true); setMsg(null);
try {
await api.clearCache();
setMsg({ ok: true, text: 'Cache cleared successfully' });
} catch (e) { setMsg({ ok: false, text: e.message }); }
finally { setClearing(false); }
}
return (
<div className="animate-slide-up flex flex-col gap-6">
<div>
<h1 className="page-title">Settings</h1>
<p className="page-subtitle">Configure ArchStore preferences</p>
</div>
{msg && (
<div className="rounded-lg p-3 flex items-center gap-2 text-xs font-medium"
style={{
background: msg.ok ? 'var(--green-muted)' : 'var(--red-muted)',
color: msg.ok ? 'var(--green)' : 'var(--red)',
border: `1px solid ${msg.ok ? 'rgba(52,211,153,0.2)' : 'rgba(248,113,113,0.2)'}`,
}}>
<CheckCircle2 size={14} />
{msg.text}
</div>
)}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* AUR Helper */}
<div className="card p-6">
<h3 className="font-semibold text-sm mb-2 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<Cpu size={16} style={{ color: 'var(--accent)' }} />
AUR Helper
</h3>
<p className="text-[12px] mb-4" style={{ color: 'var(--text-tertiary)' }}>
The helper tool utilized to build and install AUR packages.
</p>
<div className="flex gap-2">
{['yay', 'paru'].map((t) => (
<div key={t} className="flex-1 p-3 rounded-lg text-center text-xs font-semibold transition-all cursor-default flex items-center justify-center gap-2"
style={{
background: t === 'yay' ? 'var(--accent-muted)' : 'var(--bg-tertiary)',
color: t === 'yay' ? 'var(--accent)' : 'var(--text-tertiary)',
border: `1px solid ${t === 'yay' ? 'rgba(56,189,248,0.2)' : 'var(--border-primary)'}`,
}}>
{t}
{t === 'yay' && <span className="text-[9px] font-bold px-1.5 py-0.5 rounded-full"
style={{ background: 'var(--green-muted)', color: 'var(--green)' }}>Active</span>}
</div>
))}
</div>
</div>
{/* Appearance */}
<div className="card p-6">
<h3 className="font-semibold text-sm mb-2 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<Palette size={16} style={{ color: 'var(--accent)' }} />
Appearance
</h3>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>Compact Density</p>
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>Optimize spacing for large lists</p>
</div>
<span
className={`toggle ${toggles.compactMode ? 'is-on' : ''}`}
onClick={() => setToggles((prev) => ({ ...prev, compactMode: !prev.compactMode }))}
></span>
</div>
</div>
</div>
{/* Notifications */}
<div className="card p-6">
<h3 className="font-semibold text-sm mb-2 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<Bell size={16} style={{ color: 'var(--accent)' }} />
Notifications
</h3>
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>Update Alerts</p>
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>Notify when new updates are available</p>
</div>
<span
className={`toggle ${toggles.notifications ? 'is-on' : ''}`}
onClick={() => setToggles((prev) => ({ ...prev, notifications: !prev.notifications }))}
></span>
</div>
</div>
{/* Security */}
<div className="card p-6">
<h3 className="font-semibold text-sm mb-2 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<ShieldCheck size={16} style={{ color: 'var(--accent)' }} />
Security
</h3>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>PKGBUILD Scans</p>
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>Run static analysis on AUR builds</p>
</div>
<span
className={`toggle ${toggles.securityScan ? 'is-on' : ''}`}
onClick={() => setToggles((prev) => ({ ...prev, securityScan: !prev.securityScan }))}
></span>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>Auto Updates</p>
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>Apply security updates automatically</p>
</div>
<span
className={`toggle ${toggles.autoUpdates ? 'is-on' : ''}`}
onClick={() => setToggles((prev) => ({ ...prev, autoUpdates: !prev.autoUpdates }))}
></span>
</div>
</div>
</div>
</div>
{/* System Controls */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div className="card p-6">
<h3 className="font-semibold text-sm mb-2 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<Database size={16} style={{ color: 'var(--accent)' }} />
Cache
</h3>
<p className="text-[12px] mb-4" style={{ color: 'var(--text-tertiary)' }}>
Search results and package metadata are cached locally to reduce API overhead.
</p>
<button onClick={clearCache} disabled={clearing} className="btn btn-danger text-xs px-3 py-1.5">
{clearing ? 'Clearing...' : 'Clear Cache'}
</button>
</div>
<div className="card p-6">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<AlertCircle size={16} style={{ color: 'var(--accent)' }} />
Backend Status
</h3>
<div className="flex items-center justify-between p-3 rounded-lg font-mono text-[11px]"
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)' }}>
<span style={{ color: 'var(--text-tertiary)' }}>FastAPI Server</span>
{health ? (
health.status === 'healthy' ? (
<span className="flex items-center gap-1.5 font-semibold" style={{ color: 'var(--green)' }}>
<span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--green)' }}></span> Online
</span>
) : (
<span className="flex items-center gap-1.5 font-semibold" style={{ color: 'var(--red)' }}>
<span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--red)' }}></span> Offline
</span>
)
) : (
<span style={{ color: 'var(--text-tertiary)' }}>{checking ? 'Checking...' : 'Unknown'}</span>
)}
</div>
</div>
</div>
</div>
);
}
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
export default function SettingsPage() {
const { theme, toggle } = useTheme();
const isDark = theme === 'dark';
const [health, setHealth] = useState<{ status: string; database: string; version: string } | null>(null);
const [healthLoading, setHealthLoading] = useState(true);
const [clearing, setClearing] = useState(false);
useEffect(() => {
api.health().then(setHealth).catch(() => setHealth(null)).finally(() => setHealthLoading(false));
}, []);
const handleClearCache = async () => {
setClearing(true);
try { await api.clearCache(); } catch {} finally { setClearing(false); }
};
const panel = `border rounded-sm ${isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'}`;
const sec = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const sel = `px-2 py-1.5 text-sm border rounded-sm ${isDark ? 'bg-dark-bg border-dark-border text-dark-text' : 'bg-light-bg border-light-border text-light-text'}`;
const hdr = `px-3 py-2 border-b text-lg font-medium ${isDark ? 'border-dark-border text-dark-text' : 'border-light-border text-light-text'}`;
const row = `flex items-center justify-between px-3 py-2.5 border-b last:border-b-0 ${isDark ? 'border-dark-border' : 'border-light-border'}`;
const btn = `px-3 py-1 text-xs border rounded-sm ${isDark ? 'border-dark-border text-dark-text-secondary hover:bg-dark-hover' : 'border-light-border text-light-text-secondary hover:bg-light-hover'} disabled:opacity-50`;
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>Settings</h1>
<div className={panel}><div className={hdr}>AUR Helper</div>
<div className={row}><div><div className="text-sm">AUR Helper</div><div className={`text-xs mt-0.5 ${sec}`}>Helper used for AUR packages</div></div>
<select defaultValue="yay" className={sel}><option value="yay">yay</option><option value="paru">paru</option><option value="trizen">trizen</option></select></div>
<div className={row}><div><div className="text-sm">Check AUR updates</div></div><input type="checkbox" defaultChecked className="accent-arch-blue" /></div>
</div>
<div className={panel}><div className={hdr}>Backend Status</div>
<div className={row}><div className="text-sm">API Status</div>
{healthLoading ? <span className={`text-sm ${sec}`}>Checking...</span> : health ? <span className="text-sm text-green-500 flex items-center gap-1.5"><span className="w-2 h-2 rounded-full bg-green-500 inline-block" />{health.status}</span> : <span className="text-sm text-red-400">Disconnected</span>}</div>
<div className={row}><div className="text-sm">Database</div><span className={`text-sm ${sec}`}>{health?.database || 'Unknown'}</span></div>
<div className={row}><div className="text-sm">API Version</div><span className={`text-sm font-mono ${sec}`}>{health?.version || '—'}</span></div>
</div>
<div className={panel}><div className={hdr}>Notifications</div>
<div className={row}><div><div className="text-sm">Desktop notifications</div><div className={`text-xs mt-0.5 ${sec}`}>Show notifications for updates</div></div><input type="checkbox" defaultChecked className="accent-arch-blue" /></div>
<div className={row}><div><div className="text-sm">Update check interval</div></div>
<select className={sel} defaultValue="6h"><option value="1h">Every hour</option><option value="6h">Every 6 hours</option><option value="24h">Daily</option><option value="never">Never</option></select></div>
</div>
<div className={panel}><div className={hdr}>Cache</div>
<div className={row}><div><div className="text-sm">Package cache</div><div className={`text-xs mt-0.5 ${sec}`}>Cached metadata (TTL: 15min)</div></div>
<button onClick={handleClearCache} disabled={clearing} className={btn}>{clearing ? 'Clearing...' : 'Clear Cache'}</button></div>
</div>
<div className={panel}><div className={hdr}>Appearance</div>
<div className={row}><div><div className="text-sm">Theme</div><div className={`text-xs mt-0.5 ${sec}`}>Current: {theme}</div></div>
<button onClick={toggle} className={btn}>{isDark ? 'Switch to Light' : 'Switch to Dark'}</button></div>
</div>
<div className={panel}><div className={hdr}>About</div>
<div className={row}><div className="text-sm">ArchStore</div><span className={`text-sm font-mono ${sec}`}>v1.0.0</span></div>
<div className={row}><div className="text-sm">License</div><span className={`text-sm ${sec}`}>GPL-3.0</span></div>
</div>
</div>
);
}
-203
View File
@@ -1,203 +0,0 @@
import { useState, useEffect } from 'react';
import api from '../api/client';
import LoadingSpinner from '../components/LoadingSpinner';
import { RefreshCw, ArrowUpCircle, Info, CheckCircle2, ArrowRight } from 'lucide-react';
export default function Updates() {
const [updates, setUpdates] = useState([]);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState(false);
const [log, setLog] = useState('');
const [error, setError] = useState(null);
useEffect(() => { check(); }, []);
async function check() {
setLoading(true); setError(null);
try { const d = await api.checkUpdates(); setUpdates(d.results || []); }
catch (e) { setError(e.message); }
finally { setLoading(false); }
}
async function handleUpdate() {
if (!confirm('Run a full system upgrade (yay -Syu)?')) return;
setUpdating(true); setError(null);
setLog('Starting system upgrade...\n');
try {
const r = await api.applyUpdates();
if (r.success) { setLog(p => p + '\n✓ Upgrade complete!\n' + r.message); setUpdates([]); }
else setError('Upgrade failed: ' + r.message);
} catch (e) { setError(e.message); }
finally { setUpdating(false); }
}
return (
<div className="animate-slide-up flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
<div>
<h1 className="page-title">System Updates</h1>
<p className="page-subtitle">Keep your system and AUR packages current</p>
</div>
<div className="flex gap-2">
<button onClick={check} disabled={loading || updating} className="btn btn-secondary text-xs">
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} /> Check
</button>
{updates.length > 0 && (
<button onClick={handleUpdate} disabled={updating} className="btn btn-primary text-xs">
<ArrowUpCircle size={12} /> Update All
</button>
)}
</div>
</div>
{error && (
<div className="rounded-lg p-3 text-xs font-medium"
style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(248,113,113,0.15)' }}>
{error}
</div>
)}
{/* Upgrade Log */}
{updating && (
<div className="card p-4" style={{ borderColor: 'var(--border-glow)' }}>
<div className="flex items-center gap-2 mb-2 text-xs font-semibold" style={{ color: 'var(--accent)' }}>
<RefreshCw size={12} className="animate-spin" /> Upgrading...
</div>
<pre className="p-3 rounded-md font-mono text-[11px] overflow-x-auto max-h-48 whitespace-pre-wrap"
style={{ background: 'var(--bg-primary)', border: '1px solid var(--border-primary)', color: 'var(--text-secondary)' }}>
{log}
</pre>
</div>
)}
{/* Overview Panels */}
<section className="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div className="card p-5">
<h3 className="text-sm font-extrabold text-white flex items-center gap-2 mb-4">
<RefreshCw size={15} style={{ color: 'var(--accent)' }} />
Update Overview
</h3>
<div className="kpi-grid">
<div className="kpi-card">
<p className="kpi-label">Pending Updates</p>
<p className="kpi-value">{updates.length}</p>
</div>
<div className="kpi-card">
<p className="kpi-label">Security Patches</p>
<p className="kpi-value">{Math.max(1, Math.floor(updates.length / 3))}</p>
</div>
<div className="kpi-card">
<p className="kpi-label">Estimated Size</p>
<p className="kpi-value">{updates.length > 0 ? `${(updates.length * 42).toFixed(0)} MB` : '0 MB'}</p>
</div>
</div>
</div>
<div className="card p-5">
<h3 className="text-sm font-extrabold text-white flex items-center gap-2 mb-4">
<Info size={15} style={{ color: 'var(--violet)' }} />
Update Stages
</h3>
<div className="flex flex-col gap-3">
<div>
<div className="flex items-center justify-between text-xs">
<span style={{ color: 'var(--text-tertiary)' }}>Package Sync</span>
<span style={{ color: 'var(--text-secondary)' }}>80%</span>
</div>
<div className="progress-track">
<div className="progress-bar" style={{ width: '80%' }}></div>
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs">
<span style={{ color: 'var(--text-tertiary)' }}>Integrity Check</span>
<span style={{ color: 'var(--text-secondary)' }}>60%</span>
</div>
<div className="progress-track">
<div className="progress-bar" style={{ width: '60%' }}></div>
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs">
<span style={{ color: 'var(--text-tertiary)' }}>Deploy</span>
<span style={{ color: 'var(--text-secondary)' }}>40%</span>
</div>
<div className="progress-track">
<div className="progress-bar" style={{ width: '40%' }}></div>
</div>
</div>
</div>
</div>
<div className="card p-5">
<h3 className="text-sm font-extrabold text-white flex items-center gap-2 mb-4">
<CheckCircle2 size={15} style={{ color: 'var(--green)' }} />
Update History
</h3>
<div className="flex flex-col gap-3 text-xs" style={{ color: 'var(--text-tertiary)' }}>
<div className="glass-panel p-3">
<p className="font-semibold" style={{ color: 'var(--text-primary)' }}>System refresh</p>
<p>37 packages updated</p>
</div>
<div className="glass-panel p-3">
<p className="font-semibold" style={{ color: 'var(--text-primary)' }}>Security patch</p>
<p>OpenSSL + systemd</p>
</div>
</div>
</div>
</section>
{/* Content */}
{loading ? (
<LoadingSpinner text="Checking for updates..." />
) : updates.length === 0 ? (
<div className="flex flex-col items-center justify-center py-14" style={{ color: 'var(--text-tertiary)' }}>
<div className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
style={{ background: 'var(--green-muted)', border: '1px solid rgba(52,211,153,0.15)' }}>
<CheckCircle2 size={18} style={{ color: 'var(--green)' }} />
</div>
<p className="text-sm font-medium mb-0.5" style={{ color: 'var(--text-secondary)' }}>All up to date</p>
<p className="text-xs">No updates available right now</p>
</div>
) : (
<div className="flex flex-col gap-3">
{/* Info banner */}
<div className="rounded-lg p-3 flex items-center gap-3 text-xs"
style={{ background: 'var(--accent-muted)', border: '1px solid var(--border-glow)' }}>
<Info size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
<div>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
{updates.length} update{updates.length !== 1 ? 's' : ''} available
</span>
<span className="ml-2" style={{ color: 'var(--text-tertiary)' }}>
Root privileges required.
</span>
</div>
</div>
{/* Update list */}
<div className="card overflow-hidden">
{updates.map((u, i) => (
<div key={`${u.source}-${u.name}`}
className="update-row"
style={{ borderBottom: i < updates.length - 1 ? '1px solid var(--border-primary)' : 'none' }}>
<div className="flex items-center gap-3 min-w-0">
<span className="font-semibold text-xs truncate" style={{ color: 'var(--text-primary)' }}>{u.name}</span>
<span className={`badge ${u.source === 'aur' ? 'badge-aur' : 'badge-pacman'}`}>
{u.source === 'aur' ? 'AUR' : 'pacman'}
</span>
</div>
<div className="flex items-center gap-2 text-[11px] font-mono shrink-0" style={{ color: 'var(--text-tertiary)' }}>
<span>{u.current_version}</span>
<ArrowRight size={10} style={{ color: 'var(--text-tertiary)' }} />
<span style={{ color: 'var(--green)', fontWeight: 600 }}>{u.new_version}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
+218
View File
@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react';
import { ArrowDownToLine, Shield, CheckCircle } from 'lucide-react';
import { useTheme } from '../lib/theme';
import { api } from '../lib/api';
import type { UpdatePackage } from '../types';
interface UpdatesProps {
onSelectPackage: (name: string) => void;
onUpdatesLoaded: (count: number) => void;
}
export default function Updates({ onSelectPackage, onUpdatesLoaded }: UpdatesProps) {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [updates, setUpdates] = useState<UpdatePackage[]>([]);
const [loading, setLoading] = useState(true);
const [applying, setApplying] = useState(false);
const [error, setError] = useState('');
const [selected, setSelected] = useState<Set<string>>(new Set());
useEffect(() => {
loadUpdates();
}, []);
const loadUpdates = async () => {
setLoading(true);
setError('');
try {
const data = await api.checkUpdates();
setUpdates(data.results);
setSelected(new Set(data.results.map(u => u.name)));
onUpdatesLoaded(data.count);
} catch (err: any) {
setError(err.message || 'Failed to check updates');
} finally {
setLoading(false);
}
};
const handleApply = async () => {
setApplying(true);
try {
await api.applyUpdates();
await loadUpdates();
} catch (err: any) {
setError(err.message || 'Update failed');
} finally {
setApplying(false);
}
};
const toggleSelect = (name: string) => {
setSelected(prev => {
const next = new Set(prev);
if (next.has(name)) next.delete(name);
else next.add(name);
return next;
});
};
const toggleAll = () => {
if (selected.size === updates.length) {
setSelected(new Set());
} else {
setSelected(new Set(updates.map(u => u.name)));
}
};
const securityUpdates = updates.filter(u =>
u.name.startsWith('linux') || u.name.includes('openssl') || u.name.includes('gnutls') ||
u.name.includes('nss') || u.name.includes('ca-certificates')
);
const regularUpdates = updates.filter(u => !securityUpdates.includes(u));
const panelClass = `border rounded-sm ${
isDark ? 'bg-dark-panel border-dark-border' : 'bg-light-panel border-light-border'
}`;
const secondaryText = isDark ? 'text-dark-text-secondary' : 'text-light-text-secondary';
const renderTable = (items: UpdatePackage[], title: string, icon: React.ReactNode) => (
<div className={panelClass}>
<div className={`px-3 py-2 border-b flex items-center justify-between ${isDark ? 'border-dark-border' : 'border-light-border'}`}>
<span className={`text-sm font-medium flex items-center gap-1.5 ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
{icon}
{title}
<span className={`text-xs ${secondaryText}`}>({items.length})</span>
</span>
</div>
{items.length === 0 ? (
<div className={`p-3 text-sm ${secondaryText}`}>None available.</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className={`text-left text-xs ${secondaryText}`}>
<th className="px-3 py-1.5 font-medium w-8">
<input
type="checkbox"
checked={items.every(u => selected.has(u.name))}
onChange={toggleAll}
className="accent-arch-blue"
/>
</th>
<th className="px-3 py-1.5 font-medium">Package</th>
<th className="px-3 py-1.5 font-medium">Current Version</th>
<th className="px-3 py-1.5 font-medium">New Version</th>
<th className="px-3 py-1.5 font-medium">Source</th>
</tr>
</thead>
<tbody>
{items.map(u => (
<tr
key={u.name}
className={`border-t cursor-pointer ${
isDark
? 'border-dark-border hover:bg-dark-hover'
: 'border-light-border hover:bg-light-hover'
}`}
onClick={() => toggleSelect(u.name)}
>
<td className="px-3 py-1.5">
<input
type="checkbox"
checked={selected.has(u.name)}
onChange={() => toggleSelect(u.name)}
className="accent-arch-blue"
/>
</td>
<td
className="px-3 py-1.5 text-arch-blue font-medium whitespace-nowrap"
onClick={e => { e.stopPropagation(); onSelectPackage(u.name); }}
>
{u.name}
</td>
<td className={`px-3 py-1.5 text-xs font-mono ${secondaryText}`}>
{u.current_version}
</td>
<td className="px-3 py-1.5 text-xs font-mono text-green-400">
{u.new_version}
</td>
<td className="px-3 py-1.5">
<span className={`text-xs px-1.5 py-0.5 rounded-sm ${
u.source === 'aur'
? 'bg-arch-blue-light text-arch-blue'
: isDark ? 'bg-dark-active text-dark-text-secondary' : 'bg-light-active text-light-text-secondary'
}`}>
{u.source}
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
return (
<div className="p-4 space-y-4 overflow-y-auto h-full">
<div className="flex items-center justify-between">
<h1 className={`text-3xl font-semibold ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
System Updates
</h1>
<div className="flex gap-2">
<button
onClick={loadUpdates}
disabled={loading}
className={`px-3 py-1.5 text-sm border rounded-sm ${
isDark
? 'border-dark-border text-dark-text-secondary hover:bg-dark-hover'
: 'border-light-border text-light-text-secondary hover:bg-light-hover'
} disabled:opacity-50`}
>
{loading ? 'Checking...' : 'Check Again'}
</button>
<button
onClick={handleApply}
disabled={applying || selected.size === 0 || updates.length === 0}
className="px-4 py-1.5 text-sm bg-arch-blue text-white rounded-sm hover:bg-arch-blue-hover disabled:opacity-50 flex items-center gap-1.5"
>
<ArrowDownToLine size={14} />
{applying ? 'Upgrading...' : `Upgrade (${selected.size})`}
</button>
</div>
</div>
{error && (
<div className="px-3 py-2 text-sm border border-red-800 bg-red-900/20 text-red-400 rounded-sm">
{error}
</div>
)}
{loading ? (
<div className={`${panelClass} p-4 text-sm ${secondaryText}`}>
Checking for updates...
</div>
) : updates.length === 0 ? (
<div className={panelClass}>
<div className="p-6 text-center">
<CheckCircle size={32} className="mx-auto mb-2 text-green-500" />
<p className={`text-sm font-medium ${isDark ? 'text-dark-text' : 'text-light-text'}`}>
System is up to date
</p>
<p className={`text-xs mt-1 ${secondaryText}`}>
All packages are at their latest versions.
</p>
</div>
</div>
) : (
<>
{securityUpdates.length > 0 &&
renderTable(securityUpdates, 'Security Updates', <Shield size={14} className="text-yellow-500" />)
}
{renderTable(regularUpdates, 'Available Updates', <ArrowDownToLine size={14} className="text-arch-blue" />)}
</>
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export interface Package {
name: string;
description: string;
version: string;
repository: string;
source: 'pacman' | 'aur';
installed: boolean;
installed_version?: string;
url?: string;
maintainer?: string;
votes?: number;
popularity?: number;
out_of_date?: boolean;
depends?: string[];
make_depends?: string[];
opt_depends?: string[];
size?: string;
install_size?: string;
licenses?: string[];
groups?: string[];
build_date?: string;
install_date?: string;
packager?: string;
arch?: string;
provides?: string[];
conflicts?: string[];
replaces?: string[];
}
export interface UpdatePackage {
name: string;
current_version: string;
new_version: string;
source: 'pacman' | 'aur';
repository?: string;
}
export interface Category {
name: string;
description: string;
icon: string;
count: number;
}
export interface SearchResult {
results: Package[];
count: number;
query: string;
source: string;
}
export interface InstalledResult {
results: Package[];
count: number;
}
export interface UpdatesResult {
results: UpdatePackage[];
count: number;
pacman_count: number;
aur_count: number;
}
export interface CategoriesResult {
results: Category[];
count: number;
}
export interface CategoryPackagesResult {
category: string;
results: Package[];
count: number;
}
export interface ScanResult {
package_name: string;
risk_score: number;
findings: ScanFinding[];
scanned: boolean;
risk_level: string;
message?: string;
}
export interface ScanFinding {
type: string;
severity: string;
description: string;
line?: number;
}
export type ThemeMode = 'dark' | 'light';
export type Page =
| 'dashboard'
| 'search'
| 'installed'
| 'updates'
| 'categories'
| 'settings'
| 'package-details';
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
@@ -2,13 +2,10 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
plugins: [react(), tailwindcss()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',